diff --git a/hedera-dependency-versions/build.gradle.kts b/hedera-dependency-versions/build.gradle.kts index 2a7665b8ba36..74a8a2150a4b 100644 --- a/hedera-dependency-versions/build.gradle.kts +++ b/hedera-dependency-versions/build.gradle.kts @@ -55,7 +55,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.14") + version("com.hedera.pbj.runtime", "0.7.19") version("com.squareup.javapoet", "1.13.0") version("com.sun.jna", "5.12.1") version("dagger", daggerVersion) diff --git a/hedera-node/config.txt b/hedera-node/config.txt index 839663e904b2..e9db3aec23f2 100755 --- a/hedera-node/config.txt +++ b/hedera-node/config.txt @@ -3,7 +3,6 @@ ############################################################################################### swirld, 123 -# app, HashgraphDemo.jar, 1,0,0,0,0,0,0,0,0,0, all # app, GameDemo.jar, 9000, 9000 # app, HelloSwirldDemo.jar # app, CryptocurrencyDemo.jar @@ -72,12 +71,6 @@ nextNodeId, 1 # # FilesystemDemo.jar parameters: none # -# HashGraphDemo.jar takes parameters that give the initial checkbox settings, -# in the same order they appear are on the screen, with 1 to check it -# and 0 to not check it, followed by the number of events to display -# (or “all”). The first parameter controls whether it runs -# slowly (1) or runs at full speed (0). -# # GameDemo.jar parameters: # height: height of the board (in cells). Player moves 1 cell at a time. # width: width of the board (in cells). Player moves 1 cell at a time. diff --git a/hedera-node/hapi-utils/src/main/java/com/hedera/node/app/hapi/utils/forensics/OrderedComparison.java b/hedera-node/hapi-utils/src/main/java/com/hedera/node/app/hapi/utils/forensics/OrderedComparison.java index e23f48a8b5c7..571700f75c21 100644 --- a/hedera-node/hapi-utils/src/main/java/com/hedera/node/app/hapi/utils/forensics/OrderedComparison.java +++ b/hedera-node/hapi-utils/src/main/java/com/hedera/node/app/hapi/utils/forensics/OrderedComparison.java @@ -31,7 +31,6 @@ import java.util.List; import java.util.Map; import java.util.function.BiFunction; -import java.util.function.Consumer; import java.util.function.Predicate; /** Provides helpers to compare and analyze record streams. */ @@ -50,7 +49,7 @@ private OrderedComparison() { * @param firstStreamDir the first record stream * @param secondStreamDir the second record stream * @param recordDiffSummarizer if present, a summarizer for record diffs - * @param fileNameObserver if set, a consumer receiving the name of each file as it is parsed + * @param maybeInclusionTest if set, a consumer receiving the name of each file as it is parsed * @return the stream diff * @throws IOException if any of the record stream files cannot be read or parsed * @throws IllegalArgumentException if the directories contain misaligned record streams @@ -59,22 +58,38 @@ public static List findDifferencesBetweenV6( @NonNull final String firstStreamDir, @NonNull final String secondStreamDir, @Nullable final RecordDiffSummarizer recordDiffSummarizer, - @Nullable final Consumer fileNameObserver) + @Nullable final Predicate maybeInclusionTest, + @Nullable final String maybeInclusionDescription) throws IOException { - final Predicate watchingPredicate = f -> { - if (fileNameObserver != null) { - fileNameObserver.accept(f); - } - return true; - }; - System.out.println("Parsing stream @ " + firstStreamDir); - final var firstEntries = parseV6RecordStreamEntriesIn(firstStreamDir, watchingPredicate); + final Predicate inclusionTest = maybeInclusionTest == null ? f -> true : maybeInclusionTest; + final String inclusionDescription = maybeInclusionDescription == null ? "all" : maybeInclusionDescription; + System.out.println("Parsing stream @ " + firstStreamDir + "(including " + inclusionDescription + ")"); + final var firstEntries = parseV6RecordStreamEntriesIn(firstStreamDir, inclusionTest); System.out.println(" ➡️ Read " + firstEntries.size() + " entries"); - System.out.println("Parsing stream @ " + secondStreamDir); - final var secondEntries = parseV6RecordStreamEntriesIn(secondStreamDir, watchingPredicate); + System.out.println("Parsing stream @ " + secondStreamDir + "(including " + inclusionDescription + ")"); + final var secondEntries = parseV6RecordStreamEntriesIn(secondStreamDir, inclusionTest); + List newSecondEntries = getNewSecondRecordStreamEntries(firstEntries, secondEntries); System.out.println(" ➡️ Read " + secondEntries.size() + " entries"); - // FUTURE: Add a step to align consensus times in the two streams when any record is missing - return diff(firstEntries, secondEntries, recordDiffSummarizer); + return diff(firstEntries, newSecondEntries, recordDiffSummarizer); + } + + @NonNull + private static List getNewSecondRecordStreamEntries( + List firstEntries, List secondEntries) { + List ret = new ArrayList<>(); + RecordStreamEntry firstEntry, secondEntry; + int secondIndex = 0; + for (RecordStreamEntry entry : firstEntries) { + firstEntry = entry; + secondEntry = secondEntries.get(secondIndex); + if (secondEntry.consensusTime().equals(firstEntry.consensusTime())) { + ret.add(secondEntry); + secondIndex++; + } else { + ret.add(new RecordStreamEntry(null, null, firstEntry.consensusTime())); + } + } + return ret; } public interface RecordDiffSummarizer extends BiFunction {} @@ -97,6 +112,14 @@ static List diff( for (int i = 0; i < minSize; i++) { final var firstEntry = firstEntries.get(i); try { + if (secondEntries.get(i).txnRecord() == null) { + diffs.add(new DifferingEntries( + firstEntry, + null, + "No record found at " + firstEntry.consensusTime() + " for transactionID : " + + firstEntry.txnRecord().getTransactionID())); + continue; + } final var secondEntry = entryWithMatchableRecord(secondEntries, i, firstEntry); if (!firstEntry.txnRecord().equals(secondEntry.txnRecord())) { final var summary = recordDiffSummarizer == null @@ -154,9 +177,6 @@ private static RecordStreamEntry entryWithMatchableRecord( @NonNull final List entries, final int i, @NonNull final RecordStreamEntry entryToMatch) throws UnmatchableException { final var secondEntry = entries.get(i); - if (secondEntry == null) { - throw new UnmatchableException("No matching entry found for entry at position " + i); - } if (!entryToMatch.consensusTime().equals(secondEntry.consensusTime())) { throw new UnmatchableException("Entries at position " + i diff --git a/hedera-node/hapi-utils/src/test/java/com/hedera/node/app/hapi/utils/forensics/OrderedComparisonTest.java b/hedera-node/hapi-utils/src/test/java/com/hedera/node/app/hapi/utils/forensics/OrderedComparisonTest.java index 8ab073193ecb..181288f4d4ec 100644 --- a/hedera-node/hapi-utils/src/test/java/com/hedera/node/app/hapi/utils/forensics/OrderedComparisonTest.java +++ b/hedera-node/hapi-utils/src/test/java/com/hedera/node/app/hapi/utils/forensics/OrderedComparisonTest.java @@ -58,7 +58,7 @@ void detectsDifferenceInCaseOfObviouslyWrongNonce() throws IOException { final var issStreamLoc = WRONG_NONCE_STREAMS_DIR + File.separator + "node5"; final var consensusStreamLoc = WRONG_NONCE_STREAMS_DIR + File.separator + "node0"; - final var diffs = findDifferencesBetweenV6(issStreamLoc, consensusStreamLoc, null, null); + final var diffs = findDifferencesBetweenV6(issStreamLoc, consensusStreamLoc, null, null, null); assertEquals(1, diffs.size()); final var soleDiff = diffs.get(0); final var issEntry = soleDiff.firstEntry(); diff --git a/hedera-node/hapi/build.gradle.kts b/hedera-node/hapi/build.gradle.kts index ba8f6c8b6543..9bfc4093709d 100644 --- a/hedera-node/hapi/build.gradle.kts +++ b/hedera-node/hapi/build.gradle.kts @@ -25,7 +25,7 @@ description = "Hedera API" // Add downloaded HAPI repo protobuf files into build directory and add to sources to build them tasks.cloneHederaProtobufs { - branchOrTag = "use-ContractID-in-SlotKey" + branchOrTag = "add-pbj-types-for-state" // As long as the 'branchOrTag' above is not stable, run always: outputs.upToDateWhen { false } } 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 aea46d14f9b4..5f0b3c2ad40a 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 @@ -39,6 +39,7 @@ import static com.hedera.node.app.state.merkle.MerkleSchemaRegistry.isSoOrdered; import static com.hedera.node.app.throttle.ThrottleAccumulator.ThrottleType.BACKEND_THROTTLE; import static com.hedera.node.app.throttle.ThrottleAccumulator.ThrottleType.FRONTEND_THROTTLE; +import static com.hedera.node.app.util.FileUtilities.observePropertiesAndPermissions; import static com.hedera.node.app.util.HederaAsciiArt.HEDERA; import static com.swirlds.platform.system.InitTrigger.EVENT_STREAM_RECOVERY; import static com.swirlds.platform.system.InitTrigger.GENESIS; @@ -1074,7 +1075,6 @@ private void initializeForTrigger( // the various migration methods may depend on configuration to do their work logger.info("Initializing Reconnect configuration"); this.configProvider = new ConfigProviderImpl(false); - logConfiguration(); logger.info("Initializing ThrottleManager"); this.throttleManager = new ThrottleManager(); @@ -1107,7 +1107,8 @@ private void initializeForTrigger( initializeExchangeRateManager(state); initializeFeeManager(state); initializeThrottles(state); - // TODO We may need to update the config with the latest version in file 121 + observePropertiesAndPermissions(state, configProvider.getConfiguration(), configProvider::update); + logConfiguration(); } /*================================================================================================================== 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 60327fff241d..b6a06ef05681 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 @@ -16,17 +16,23 @@ package com.hedera.node.app.bbm; +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.nfts.UniqueTokenDumpUtils.dumpModUniqueTokens; import static com.hedera.node.app.bbm.nfts.UniqueTokenDumpUtils.dumpMonoUniqueTokens; import static com.hedera.node.app.records.BlockRecordService.BLOCK_INFO_STATE_KEY; import static com.hedera.node.app.service.mono.state.migration.StateChildIndices.NETWORK_CTX; +import static com.hedera.node.app.service.mono.state.migration.StateChildIndices.TOKEN_ASSOCIATIONS; import static com.hedera.node.app.service.mono.state.migration.StateChildIndices.UNIQUE_TOKENS; import static com.hedera.node.app.service.token.impl.TokenServiceImpl.NFTS_KEY; +import static com.hedera.node.app.service.token.impl.TokenServiceImpl.TOKEN_RELS_KEY; import static java.util.Objects.requireNonNull; 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.token.Nft; +import com.hedera.hapi.node.state.token.TokenRelation; import com.hedera.node.app.records.BlockRecordService; import com.hedera.node.app.service.mono.state.merkle.MerkleNetworkContext; import com.hedera.node.app.service.token.TokenService; @@ -47,12 +53,15 @@ */ public class StateDumper { private static final String SEMANTIC_UNIQUE_TOKENS = "uniqueTokens.txt"; + private static final String SEMANTIC_TOKEN_RELATIONS = "tokenRelations.txt"; public static void dumpMonoChildrenFrom( @NonNull final MerkleHederaState state, @NonNull final DumpCheckpoint checkpoint) { final MerkleNetworkContext networkContext = state.getChild(NETWORK_CTX); final var dumpLoc = getExtantDumpLoc("mono", networkContext.consensusTimeOfLastHandledTxn()); dumpMonoUniqueTokens(Paths.get(dumpLoc, SEMANTIC_UNIQUE_TOKENS), state.getChild(UNIQUE_TOKENS), checkpoint); + dumpMonoTokenRelations( + Paths.get(dumpLoc, SEMANTIC_TOKEN_RELATIONS), state.getChild(TOKEN_ASSOCIATIONS), checkpoint); } public static void dumpModChildrenFrom( @@ -68,6 +77,9 @@ public static void dumpModChildrenFrom( final VirtualMap, OnDiskValue> uniqueTokens = requireNonNull(state.getChild(state.findNodeIndex(TokenService.NAME, NFTS_KEY))); dumpModUniqueTokens(Paths.get(dumpLoc, SEMANTIC_UNIQUE_TOKENS), uniqueTokens, checkpoint); + final VirtualMap, OnDiskValue> tokenRelations = + requireNonNull(state.getChild(state.findNodeIndex(TokenService.NAME, TOKEN_RELS_KEY))); + dumpModTokenRelations(Paths.get(dumpLoc, SEMANTIC_TOKEN_RELATIONS), tokenRelations, checkpoint); } private static String getExtantDumpLoc( diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/associations/TokenAssociation.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/associations/TokenAssociation.java new file mode 100644 index 000000000000..ffbab0e0bbf4 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/associations/TokenAssociation.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.bbm.associations; + +import com.hedera.hapi.node.state.token.TokenRelation; +import com.hedera.node.app.service.mono.state.submerkle.EntityId; +import com.hedera.node.app.service.mono.state.virtual.entities.OnDiskTokenRel; +import com.hedera.node.app.service.mono.utils.EntityNumPair; +import com.hedera.node.app.state.merkle.disk.OnDiskValue; +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.TokenID; +import com.swirlds.base.utility.Pair; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +record TokenAssociation( + EntityId accountId, + EntityId tokenId, + long balance, + boolean isFrozen, + boolean isKycGranted, + boolean isAutomaticAssociation, + EntityId prev, + EntityId next) { + + @NonNull + static TokenAssociation fromMono(@NonNull final OnDiskTokenRel tokenRel) { + final var at = toLongsPair(toPair(tokenRel.getKey())); + + return new TokenAssociation( + entityIdFrom(at.left()), + entityIdFrom(at.right()), + tokenRel.getBalance(), + tokenRel.isFrozen(), + tokenRel.isKycGranted(), + tokenRel.isAutomaticAssociation(), + entityIdFrom(tokenRel.getPrev()), + entityIdFrom(tokenRel.getNext())); + } + + static TokenAssociation fromMod(@NonNull final OnDiskValue wrapper) { + final var value = wrapper.getValue(); + return new TokenAssociation( + accountIdFromMod(value.accountId()), + tokenIdFromMod(value.tokenId()), + value.balance(), + value.frozen(), + value.kycGranted(), + value.automaticAssociation(), + tokenIdFromMod(value.previousToken()), + tokenIdFromMod(value.nextToken())); + } + + @NonNull + static Pair toPair(@NonNull final EntityNumPair enp) { + final var at = enp.asAccountTokenRel(); + return Pair.of(at.getLeft(), at.getRight()); + } + + @NonNull + static Pair toLongsPair(@NonNull final Pair pat) { + return Pair.of(pat.left().getAccountNum(), pat.right().getTokenNum()); + } + + static EntityId accountIdFromMod(@Nullable final com.hedera.hapi.node.base.AccountID accountId) { + return null == accountId ? EntityId.MISSING_ENTITY_ID : new EntityId(0L, 0L, accountId.accountNumOrThrow()); + } + + static EntityId tokenIdFromMod(@Nullable final com.hedera.hapi.node.base.TokenID tokenId) { + return null == tokenId ? EntityId.MISSING_ENTITY_ID : new EntityId(0L, 0L, tokenId.tokenNum()); + } + + static EntityId entityIdFrom(long num) { + return new EntityId(0L, 0L, num); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/associations/TokenAssociationId.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/associations/TokenAssociationId.java new file mode 100644 index 000000000000..cf1d05af3341 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/associations/TokenAssociationId.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.associations; + +import static com.hedera.node.app.bbm.associations.TokenAssociation.toLongsPair; +import static com.hedera.node.app.bbm.associations.TokenAssociation.toPair; + +import com.google.common.collect.ComparisonChain; +import com.hedera.node.app.bbm.utils.Writer; +import com.hedera.node.app.service.mono.state.virtual.entities.OnDiskTokenRel; +import com.hedera.node.app.state.merkle.disk.OnDiskKey; +import edu.umd.cs.findbugs.annotations.NonNull; + +record TokenAssociationId(long accountId, long tokenId) implements Comparable { + static TokenAssociationId fromMod(@NonNull final com.hedera.hapi.node.base.TokenAssociation association) { + return new TokenAssociationId( + association.accountId().accountNum(), association.tokenId().tokenNum()); + } + + static TokenAssociationId fromMono(@NonNull final OnDiskKey tokenRel) { + final var key = toLongsPair(toPair(tokenRel.getKey().getKey())); + ; + return new TokenAssociationId(key.left(), key.right()); + } + + @Override + public String toString() { + return "%d%s%d".formatted(accountId, Writer.FIELD_SEPARATOR, tokenId); + } + + @Override + public int compareTo(TokenAssociationId o) { + return ComparisonChain.start() + .compare(this.accountId, o.accountId) + .compare(this.tokenId, o.tokenId) + .result(); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/associations/TokenAssociationsDumpUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/associations/TokenAssociationsDumpUtils.java new file mode 100644 index 000000000000..be476f6d7d2f --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/bbm/associations/TokenAssociationsDumpUtils.java @@ -0,0 +1,148 @@ +/* + * 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.associations; + +import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; + +import com.hedera.hapi.node.state.token.TokenRelation; +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.entities.OnDiskTokenRel; +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.virtualmap.VirtualKey; +import com.swirlds.virtualmap.VirtualMap; +import com.swirlds.virtualmap.VirtualValue; +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.concurrent.ConcurrentLinkedQueue; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class TokenAssociationsDumpUtils { + public static void dumpModTokenRelations( + @NonNull final Path path, + @NonNull + final VirtualMap, OnDiskValue> + associations, + @NonNull final DumpCheckpoint checkpoint) { + try (@NonNull final var writer = new Writer(path)) { + final var dumpableTokenRelations = gatherTokenRelations( + associations, key -> TokenAssociationId.fromMod(key.getKey()), TokenAssociation::fromMod); + reportOnTokenAssociations(writer, dumpableTokenRelations); + System.out.printf( + "=== mod token associations report is %d bytes at checkpoint %s%n", + writer.getSize(), checkpoint.name()); + } + } + + public static void dumpMonoTokenRelations( + @NonNull final Path path, + @NonNull final VirtualMap, OnDiskTokenRel> associations, + @NonNull final DumpCheckpoint checkpoint) { + try (@NonNull final var writer = new Writer(path)) { + final var dumpableTokenRelations = + gatherTokenRelations(associations, TokenAssociationId::fromMono, TokenAssociation::fromMono); + reportOnTokenAssociations(writer, dumpableTokenRelations); + System.out.printf( + "=== mono token associations report is %d bytes at checkpoint %s%n", + writer.getSize(), checkpoint.name()); + } + } + + @NonNull + private static + Map gatherTokenRelations( + @NonNull final VirtualMap source, + @NonNull final Function keyMapper, + @NonNull final Function valueMapper) { + final var r = new HashMap(); + final var threadCount = 8; + final var tokenAssociations = new ConcurrentLinkedQueue>(); + try { + VirtualMapLike.from(source) + .extractVirtualMapData( + getStaticThreadManager(), + p -> tokenAssociations.add( + Pair.of(keyMapper.apply(p.left()), valueMapper.apply(p.right()))), + threadCount); + } catch (final InterruptedException ex) { + System.err.println("*** Traversal of token associations virtual map interrupted!"); + Thread.currentThread().interrupt(); + } + tokenAssociations.forEach( + tokenAssociationPair -> r.put(tokenAssociationPair.key(), tokenAssociationPair.value())); + return r; + } + + private static void reportOnTokenAssociations( + @NonNull final Writer writer, @NonNull final Map associations) { + writer.writeln(formatHeader()); + associations.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(Writer.FIELD_SEPARATOR)); + } + + // spotless:off + @NonNull + private static final List>> fieldFormatters = List.of( + Pair.of("accountId", getFieldFormatter(TokenAssociation::accountId, ThingsToStrings::toStringOfEntityId)), + Pair.of("tokenId", getFieldFormatter(TokenAssociation::tokenId, ThingsToStrings::toStringOfEntityId)), + Pair.of("balance", getFieldFormatter(TokenAssociation::balance, Object::toString)), + Pair.of("isFrozen", getFieldFormatter(TokenAssociation::isFrozen, Object::toString)), + Pair.of("isKycGranted", getFieldFormatter(TokenAssociation::isKycGranted, Object::toString)), + Pair.of("isAutomaticAssociation", getFieldFormatter(TokenAssociation::isAutomaticAssociation, Object::toString)), + Pair.of("prev", getFieldFormatter(TokenAssociation::prev, ThingsToStrings::toStringOfEntityId)), + Pair.of("next", getFieldFormatter(TokenAssociation::next, ThingsToStrings::toStringOfEntityId)) + ); + // 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 TokenAssociation tokenAssociation, + @NonNull final Function fun, + @NonNull final Function formatter) { + fb.append(formatter.apply(fun.apply(tokenAssociation))); + } + + private static void formatTokenAssociation( + @NonNull final Writer writer, @NonNull final TokenAssociation tokenAssociation) { + final var fb = new FieldBuilder(Writer.FIELD_SEPARATOR); + fieldFormatters.stream().map(Pair::right).forEach(ff -> ff.accept(fb, tokenAssociation)); + writer.writeln(fb); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/logging/TransactionStateLogger.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/logging/TransactionStateLogger.java index 80ab27736655..a75caa9103f6 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/logging/TransactionStateLogger.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/logging/TransactionStateLogger.java @@ -16,8 +16,7 @@ package com.hedera.node.app.state.logging; -import static com.swirlds.platform.SwirldsPlatform.PLATFORM_THREAD_POOL_NAME; -import static com.swirlds.platform.eventhandling.ConsensusRoundHandler.THREAD_CONS_NAME; +import static com.swirlds.platform.eventhandling.ConsensusRoundHandler.TRANSACTION_HANDLING_THREAD_NAME; import com.hedera.hapi.node.base.*; import com.hedera.hapi.node.transaction.TransactionBody; @@ -60,8 +59,6 @@ public final class TransactionStateLogger { /** The logger we are using for Transaction State log */ private static final Logger logger = LogManager.getLogger(TransactionStateLogger.class); - /** The name of the handle transaction thread */ - private static final String HANDLE_THREAD_NAME = "<" + PLATFORM_THREAD_POOL_NAME + ": " + THREAD_CONS_NAME; /** * Log the start of a round if it contains any non-system transactions. @@ -235,7 +232,7 @@ public static void logEndTransactionRecord( * @param The type of the singleton */ public static void logSingletonRead(@NonNull final String label, @Nullable final ValueLeaf value) { - if (logger.isDebugEnabled() && Thread.currentThread().getName().startsWith(HANDLE_THREAD_NAME)) { + if (logger.isDebugEnabled() && Thread.currentThread().getName().equals(TRANSACTION_HANDLING_THREAD_NAME)) { logger.debug(" READ singleton {} value {}", label, value == null ? "null" : value.getValue()); } } @@ -247,7 +244,7 @@ public static void logSingletonRead(@NonNull final String label, @Nullable f * @param value The value of the singleton */ public static void logSingletonWrite(@NonNull final String label, @Nullable final Object value) { - if (logger.isDebugEnabled() && Thread.currentThread().getName().startsWith(HANDLE_THREAD_NAME)) { + if (logger.isDebugEnabled() && Thread.currentThread().getName().equals(TRANSACTION_HANDLING_THREAD_NAME)) { logger.debug(" WRITTEN singleton {} value {}", label, value == null ? "null" : value.toString()); } } @@ -261,7 +258,7 @@ public static void logSingletonWrite(@NonNull final String label, @Nullable fina * @param value The value added to the queue */ public static void logQueueAdd(@NonNull final String label, @Nullable final Object value) { - if (logger.isDebugEnabled() && Thread.currentThread().getName().startsWith(HANDLE_THREAD_NAME)) { + if (logger.isDebugEnabled() && Thread.currentThread().getName().equals(TRANSACTION_HANDLING_THREAD_NAME)) { logger.debug(" ADD to queue {} value {}", label, value == null ? "null" : value.toString()); } } @@ -273,7 +270,7 @@ public static void logQueueAdd(@NonNull final String label, @Nullable final Obje * @param value The value removed from the queue */ public static void logQueueRemove(@NonNull final String label, @Nullable final Object value) { - if (logger.isDebugEnabled() && Thread.currentThread().getName().startsWith(HANDLE_THREAD_NAME)) { + if (logger.isDebugEnabled() && Thread.currentThread().getName().equals(TRANSACTION_HANDLING_THREAD_NAME)) { logger.debug(" REMOVE from queue {} value {}", label, value == null ? "null" : value.toString()); } } @@ -285,7 +282,7 @@ public static void logQueueRemove(@NonNull final String label, @Nullable final O * @param value The value peeked from the queue */ public static void logQueuePeek(@NonNull final String label, @Nullable final Object value) { - if (logger.isDebugEnabled() && Thread.currentThread().getName().startsWith(HANDLE_THREAD_NAME)) { + if (logger.isDebugEnabled() && Thread.currentThread().getName().equals(TRANSACTION_HANDLING_THREAD_NAME)) { logger.debug(" PEEK on queue {} value {}", label, value == null ? "null" : value.toString()); } } @@ -298,7 +295,7 @@ public static void logQueuePeek(@NonNull final String label, @Nullable final Obj * @param The type of the queue values */ public static void logQueueIterate(@NonNull final String label, @NonNull final FCQueue> queue) { - if (logger.isDebugEnabled() && Thread.currentThread().getName().startsWith(HANDLE_THREAD_NAME)) { + if (logger.isDebugEnabled() && Thread.currentThread().getName().equals(TRANSACTION_HANDLING_THREAD_NAME)) { if (queue.size() == 0) { logger.debug(" ITERATE queue {} size 0 values:EMPTY", label); } else { @@ -325,7 +322,7 @@ public static void logQueueIterate(@NonNull final String label, @NonNull fin * @param The type of the value */ public static void logMapPut(@NonNull final String label, @NonNull final K key, @Nullable final V value) { - if (logger.isDebugEnabled() && Thread.currentThread().getName().startsWith(HANDLE_THREAD_NAME)) { + if (logger.isDebugEnabled() && Thread.currentThread().getName().equals(TRANSACTION_HANDLING_THREAD_NAME)) { logger.debug( " PUT into map {} key {} value {}", label, @@ -345,7 +342,7 @@ public static void logMapPut(@NonNull final String label, @NonNull final */ public static void logMapRemove( @NonNull final String label, @NonNull final K key, @Nullable final InMemoryValue value) { - if (logger.isDebugEnabled() && Thread.currentThread().getName().startsWith(HANDLE_THREAD_NAME)) { + if (logger.isDebugEnabled() && Thread.currentThread().getName().equals(TRANSACTION_HANDLING_THREAD_NAME)) { logger.debug( " REMOVE from map {} key {} removed value {}", label, @@ -365,7 +362,7 @@ public static void logMapRemove( */ public static void logMapRemove( @NonNull final String label, @NonNull final K key, @Nullable final OnDiskValue value) { - if (logger.isDebugEnabled() && Thread.currentThread().getName().startsWith(HANDLE_THREAD_NAME)) { + if (logger.isDebugEnabled() && Thread.currentThread().getName().equals(TRANSACTION_HANDLING_THREAD_NAME)) { logger.debug( " REMOVE from map {} key {} removed value {}", label, @@ -381,7 +378,7 @@ public static void logMapRemove( * @param size The size of the map */ public static void logMapGetSize(@NonNull final String label, final long size) { - if (logger.isDebugEnabled() && Thread.currentThread().getName().startsWith(HANDLE_THREAD_NAME)) { + if (logger.isDebugEnabled() && Thread.currentThread().getName().equals(TRANSACTION_HANDLING_THREAD_NAME)) { logger.debug(" GET_SIZE on map {} size {}", label, size); } } @@ -396,7 +393,7 @@ public static void logMapGetSize(@NonNull final String label, final long size) { * @param The type of the value */ public static void logMapGet(@NonNull final String label, @NonNull final K key, @Nullable final V value) { - if (logger.isDebugEnabled() && Thread.currentThread().getName().startsWith(HANDLE_THREAD_NAME)) { + if (logger.isDebugEnabled() && Thread.currentThread().getName().equals(TRANSACTION_HANDLING_THREAD_NAME)) { logger.debug( " GET on map {} key {} value {}", label, @@ -416,7 +413,7 @@ public static void logMapGet(@NonNull final String label, @NonNull final */ public static void logMapGetForModify( @NonNull final String label, @NonNull final K key, @Nullable final V value) { - if (logger.isDebugEnabled() && Thread.currentThread().getName().startsWith(HANDLE_THREAD_NAME)) { + if (logger.isDebugEnabled() && Thread.currentThread().getName().equals(TRANSACTION_HANDLING_THREAD_NAME)) { logger.debug( " GET_FOR_MODIFY on map {} key {} value {}", label, @@ -433,7 +430,7 @@ public static void logMapGetForModify( * @param The type of the key */ public static void logMapIterate(@NonNull final String label, @NonNull final Set> keySet) { - if (logger.isDebugEnabled() && Thread.currentThread().getName().startsWith(HANDLE_THREAD_NAME)) { + if (logger.isDebugEnabled() && Thread.currentThread().getName().equals(TRANSACTION_HANDLING_THREAD_NAME)) { final long size = keySet.size(); if (size == 0) { logger.debug(" ITERATE map {} size 0 keys:EMPTY", label); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/util/FileUtilities.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/util/FileUtilities.java index 8d618f0af504..3fec183fa6b2 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/util/FileUtilities.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/util/FileUtilities.java @@ -17,12 +17,16 @@ package com.hedera.node.app.util; import static com.hedera.node.app.service.file.impl.FileServiceImpl.BLOBS_KEY; +import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.FileID; import com.hedera.hapi.node.state.file.File; import com.hedera.node.app.service.file.FileService; import com.hedera.node.app.state.HederaState; +import com.hedera.node.config.data.FilesConfig; +import com.hedera.node.config.data.HederaConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.config.api.Configuration; import edu.umd.cs.findbugs.annotations.NonNull; public class FileUtilities { @@ -38,4 +42,58 @@ public static Bytes getFileContent(@NonNull final HederaState state, @NonNull fi final var file = filesMap.get(fileID); return file != null ? file.contents() : Bytes.EMPTY; } + + /** + * Observes the properties and permissions of the network from a saved state. + */ + public interface SpecialFilesObserver { + /** + * Accepts the properties and permissions of the network from a saved state. + * + * @param properties the raw bytes of the properties file + * @param permissions the raw bytes of the permissions file + */ + void acceptPropertiesAndPermissions(Bytes properties, Bytes permissions); + } + + /** + * Given a state, configuration, and observer, observes the properties and + * permissions of the network. + * + * @param state the state to observe + * @param config the configuration to use + * @param observer the observer to notify + */ + public static void observePropertiesAndPermissions( + @NonNull final HederaState state, + @NonNull final Configuration config, + @NonNull final SpecialFilesObserver observer) { + requireNonNull(state); + requireNonNull(config); + requireNonNull(observer); + final var filesConfig = config.getConfigData(FilesConfig.class); + observePropertiesAndPermissions( + state, + FileUtilities.createFileID(filesConfig.networkProperties(), config), + FileUtilities.createFileID(filesConfig.hapiPermissions(), config), + observer); + } + + private static void observePropertiesAndPermissions( + @NonNull final HederaState state, + @NonNull final FileID propertiesId, + @NonNull final FileID permissionsId, + @NonNull final SpecialFilesObserver observer) { + observer.acceptPropertiesAndPermissions( + getFileContent(state, propertiesId), getFileContent(state, permissionsId)); + } + + private static FileID createFileID(final long fileNum, @NonNull final Configuration configuration) { + final var hederaConfig = configuration.getConfigData(HederaConfig.class); + return FileID.newBuilder() + .realmNum(hederaConfig.realm()) + .shardNum(hederaConfig.shard()) + .fileNum(fileNum) + .build(); + } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ParseExceptionWorkaround.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ParseExceptionWorkaround.java deleted file mode 100644 index cdd90ed6afd5..000000000000 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/ParseExceptionWorkaround.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.node.app.workflows; - -import com.hedera.pbj.runtime.ParseException; -import edu.umd.cs.findbugs.annotations.NonNull; - -/** - * This is a temporary workaround for an unexpected behavior in PBJ. A {@link ParseException} may be wrapped in - * another {@link ParseException} as its cause. This class provides a method to get the root cause of the - * {@link ParseException}. - * - *

If we agree that {@link ParseException} should not be wrapped in another {@link ParseException}, then this class - * can be removed. - */ -public class ParseExceptionWorkaround { - - private ParseExceptionWorkaround() {} - - /** - * Returns the root cause of the given {@link ParseException}. - * - * @param ex the {@link ParseException} to get the root cause of - * @return the root cause of the given {@link ParseException} - */ - public static Throwable getParseExceptionCause(@NonNull final ParseException ex) { - var cause = ex.getCause(); - while (cause instanceof ParseException) { - cause = cause.getCause(); - } - return cause; - } -} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/TransactionChecker.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/TransactionChecker.java index 1d262533e8ec..7f4509d0e0a0 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/TransactionChecker.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/TransactionChecker.java @@ -30,7 +30,6 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.TRANSACTION_HAS_UNKNOWN_FIELDS; import static com.hedera.hapi.node.base.ResponseCodeEnum.TRANSACTION_ID_FIELD_NOT_ALLOWED; import static com.hedera.hapi.node.base.ResponseCodeEnum.TRANSACTION_OVERSIZE; -import static com.hedera.node.app.workflows.ParseExceptionWorkaround.getParseExceptionCause; import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; @@ -466,11 +465,7 @@ private T parseStrict( try { return codec.parseStrict(data); } catch (ParseException e) { - - // Temporary workaround for unexpected behavior in PBJ. Can be removed if we agree that - // ParseException should not be wrapped. - final var cause = getParseExceptionCause(e); - if (cause instanceof UnknownFieldException) { + if (e.getCause() instanceof UnknownFieldException) { // We do not allow newer clients to send transactions to older networks. throw new PreCheckException(TRANSACTION_HAS_UNKNOWN_FIELDS); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/SystemFileUpdateFacility.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/SystemFileUpdateFacility.java index 2f4f968354d2..6d708692ad9a 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/SystemFileUpdateFacility.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/SystemFileUpdateFacility.java @@ -17,6 +17,7 @@ package com.hedera.node.app.workflows.handle; import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; +import static com.hedera.node.app.util.FileUtilities.observePropertiesAndPermissions; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.joining; @@ -33,7 +34,6 @@ import com.hedera.node.app.throttle.ThrottleManager; import com.hedera.node.app.util.FileUtilities; import com.hedera.node.config.data.FilesConfig; -import com.hedera.node.config.data.HederaConfig; import com.hedera.node.config.data.LedgerConfig; import com.hedera.pbj.runtime.ParseException; import com.hedera.pbj.runtime.io.buffer.Bytes; @@ -115,31 +115,23 @@ public ResponseCodeEnum handleTxBody(@NonNull final HederaState state, @NonNull // If it is a special file, call the updater. // We load the file only, if there is an updater for it. - final var config = configuration.getConfigData(FilesConfig.class); + final var filesConfig = configuration.getConfigData(FilesConfig.class); - if (fileNum == config.feeSchedules()) { + if (fileNum == filesConfig.feeSchedules()) { return feeManager.update(FileUtilities.getFileContent(state, fileID)); - } else if (fileNum == config.exchangeRates()) { + } else if (fileNum == filesConfig.exchangeRates()) { exchangeRateManager.update(FileUtilities.getFileContent(state, fileID), payer); - } else if (fileNum == config.networkProperties()) { - final var networkProperties = FileUtilities.getFileContent(state, fileID); - final var permissions = - FileUtilities.getFileContent(state, createFileID(config.hapiPermissions(), configuration)); - configProvider.update(networkProperties, permissions); - logContentsOf("Network properties", networkProperties); + } else if (fileNum == filesConfig.networkProperties()) { + updateConfig(configuration, ConfigType.NETWORK_PROPERTIES, state); backendThrottle.applyGasConfig(); frontendThrottle.applyGasConfig(); // Updating the multiplier source to use the new gas throttle // values that are coming from the network properties congestionMultipliers.resetExpectations(); - } else if (fileNum == config.hapiPermissions()) { - final var networkProperties = - FileUtilities.getFileContent(state, createFileID(config.networkProperties(), configuration)); - final var permissions = FileUtilities.getFileContent(state, fileID); - configProvider.update(networkProperties, permissions); - logContentsOf("API permissions", permissions); - } else if (fileNum == config.throttleDefinitions()) { + } else if (fileNum == filesConfig.hapiPermissions()) { + updateConfig(configuration, ConfigType.API_PERMISSIONS, state); + } else if (fileNum == filesConfig.throttleDefinitions()) { final var result = throttleManager.update(FileUtilities.getFileContent(state, fileID)); backendThrottle.rebuildFor(throttleManager.throttleDefinitions()); frontendThrottle.rebuildFor(throttleManager.throttleDefinitions()); @@ -151,6 +143,25 @@ public ResponseCodeEnum handleTxBody(@NonNull final HederaState state, @NonNull return SUCCESS; } + private enum ConfigType { + NETWORK_PROPERTIES, + API_PERMISSIONS, + } + + private void updateConfig( + @NonNull final Configuration configuration, + @NonNull final ConfigType configType, + @NonNull final HederaState state) { + observePropertiesAndPermissions(state, configuration, (properties, permissions) -> { + configProvider.update(properties, permissions); + if (configType == ConfigType.NETWORK_PROPERTIES) { + logContentsOf("Network properties", properties); + } else { + logContentsOf("API permissions", permissions); + } + }); + } + private void logContentsOf(@NonNull final String configFileName, @NonNull final Bytes contents) { try { final var configList = ServicesConfigurationList.PROTOBUF.parseStrict(contents.toReadableSequentialData()); @@ -165,13 +176,4 @@ private void logContentsOf(@NonNull final String configFileName, @NonNull final // If this isn't parseable we won't have updated anything, also don't log } } - - private FileID createFileID(final long fileNum, @NonNull final Configuration configuration) { - final var hederaConfig = configuration.getConfigData(HederaConfig.class); - return FileID.newBuilder() - .realmNum(hederaConfig.realm()) - .shardNum(hederaConfig.shard()) - .fileNum(fileNum) - .build(); - } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java index f2f0a58c8801..e67ba1a2e888 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java @@ -24,7 +24,6 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.PAYER_ACCOUNT_NOT_FOUND; import static com.hedera.hapi.node.base.ResponseType.ANSWER_STATE_PROOF; import static com.hedera.hapi.node.base.ResponseType.COST_ANSWER_STATE_PROOF; -import static com.hedera.node.app.workflows.ParseExceptionWorkaround.getParseExceptionCause; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountID; @@ -302,12 +301,7 @@ private Query parseQuery(Bytes requestBuffer) { try { return queryParser.parseStrict(requestBuffer.toReadableSequentialData()); } catch (ParseException e) { - - // Temporary workaround for unexpected behavior in PBJ. Can be removed if we agree that - // ParseException should not be wrapped. - final var cause = getParseExceptionCause(e); - - switch (cause) { + switch (e.getCause()) { case MalformedProtobufException ex: break; case UnknownFieldException ex: diff --git a/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/EvmEncodingFacade.java b/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/EvmEncodingFacade.java index a2083c08e7ae..cab2c1fe603c 100644 --- a/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/EvmEncodingFacade.java +++ b/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/EvmEncodingFacade.java @@ -35,6 +35,7 @@ import com.esaulpaugh.headlong.abi.Tuple; import com.esaulpaugh.headlong.abi.TupleType; import com.hedera.node.app.service.evm.store.contracts.utils.EvmParsingConstants.FunctionType; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import edu.umd.cs.findbugs.annotations.NonNull; import java.math.BigInteger; import java.util.ArrayList; @@ -60,9 +61,13 @@ public Bytes encodeDecimals(final int decimals) { } public Bytes encodeGetTokenType(final int tokenType) { + return encodeGetTokenType(SUCCESS, tokenType); + } + + public Bytes encodeGetTokenType(final ResponseCodeEnum status, final int tokenType) { return functionResultBuilder() .forFunction(FunctionType.HAPI_GET_TOKEN_TYPE) - .withStatus(SUCCESS.getNumber()) + .withStatus(status.getNumber()) .withGetTokenType(tokenType) .build(); } @@ -96,41 +101,61 @@ public Bytes encodeIsApprovedForAll(final boolean isApprovedForAllStatus) { } public Bytes encodeIsFrozen(final boolean isFrozen) { + return encodeIsFrozen(SUCCESS, isFrozen); + } + + public Bytes encodeIsFrozen(final ResponseCodeEnum status, final boolean isFrozen) { return functionResultBuilder() .forFunction(FunctionType.HAPI_IS_FROZEN) - .withStatus(SUCCESS.getNumber()) + .withStatus(status.getNumber()) .withIsFrozen(isFrozen) .build(); } public Bytes encodeGetTokenDefaultFreezeStatus(final boolean defaultFreezeStatus) { + return encodeGetTokenDefaultFreezeStatus(SUCCESS, defaultFreezeStatus); + } + + public Bytes encodeGetTokenDefaultFreezeStatus(final ResponseCodeEnum status, final boolean defaultFreezeStatus) { return functionResultBuilder() .forFunction(FunctionType.GET_TOKEN_DEFAULT_FREEZE_STATUS) - .withStatus(SUCCESS.getNumber()) + .withStatus(status.getNumber()) .withGetTokenDefaultFreezeStatus(defaultFreezeStatus) .build(); } public Bytes encodeGetTokenDefaultKycStatus(final boolean defaultKycStatus) { + return encodeGetTokenDefaultKycStatus(SUCCESS, defaultKycStatus); + } + + public Bytes encodeGetTokenDefaultKycStatus(final ResponseCodeEnum status, final boolean defaultKycStatus) { return functionResultBuilder() .forFunction(FunctionType.GET_TOKEN_DEFAULT_KYC_STATUS) - .withStatus(SUCCESS.getNumber()) + .withStatus(status.getNumber()) .withGetTokenDefaultKycStatus(defaultKycStatus) .build(); } public Bytes encodeIsKyc(final boolean isKyc) { + return encodeIsKyc(SUCCESS, isKyc); + } + + public Bytes encodeIsKyc(final ResponseCodeEnum status, final boolean isKyc) { return functionResultBuilder() .forFunction(FunctionType.HAPI_IS_KYC) - .withStatus(SUCCESS.getNumber()) + .withStatus(status.getNumber()) .withIsKyc(isKyc) .build(); } public Bytes encodeIsToken(final boolean isToken) { + return encodeIsToken(SUCCESS, isToken); + } + + public Bytes encodeIsToken(final ResponseCodeEnum status, final boolean isToken) { return functionResultBuilder() .forFunction(FunctionType.HAPI_IS_TOKEN) - .withStatus(SUCCESS.getNumber()) + .withStatus(status.getNumber()) .withIsToken(isToken) .build(); } @@ -171,50 +196,75 @@ public Bytes encodeGetApproved(final Address approved) { } public Bytes encodeGetTokenInfo(final EvmTokenInfo tokenInfo) { + return encodeGetTokenInfo(SUCCESS, tokenInfo); + } + + public Bytes encodeGetTokenInfo(final ResponseCodeEnum status, final EvmTokenInfo tokenInfo) { return functionResultBuilder() .forFunction(FunctionType.HAPI_GET_TOKEN_INFO) - .withStatus(SUCCESS.getNumber()) + .withStatus(status.getNumber()) .withTokenInfo(tokenInfo) .build(); } public Bytes encodeGetFungibleTokenInfo(final EvmTokenInfo tokenInfo) { + return encodeGetFungibleTokenInfo(SUCCESS, tokenInfo); + } + + public Bytes encodeGetFungibleTokenInfo(final ResponseCodeEnum status, final EvmTokenInfo tokenInfo) { return functionResultBuilder() .forFunction(FunctionType.HAPI_GET_FUNGIBLE_TOKEN_INFO) - .withStatus(SUCCESS.getNumber()) + .withStatus(status.getNumber()) .withTokenInfo(tokenInfo) .build(); } public Bytes encodeTokenGetCustomFees(final List customFees) { + return encodeTokenGetCustomFees(SUCCESS, customFees); + } + + public Bytes encodeTokenGetCustomFees(final ResponseCodeEnum status, final List customFees) { return functionResultBuilder() .forFunction(FunctionType.HAPI_GET_TOKEN_CUSTOM_FEES) - .withStatus(SUCCESS.getNumber()) + .withStatus(status.getNumber()) .withCustomFees(customFees) .build(); } public Bytes encodeGetNonFungibleTokenInfo(final EvmTokenInfo tokenInfo, final EvmNftInfo nonFungibleTokenInfo) { + return encodeGetNonFungibleTokenInfo(SUCCESS, tokenInfo, nonFungibleTokenInfo); + } + + public Bytes encodeGetNonFungibleTokenInfo( + final ResponseCodeEnum status, final EvmTokenInfo tokenInfo, final EvmNftInfo nonFungibleTokenInfo) { return functionResultBuilder() .forFunction(FunctionType.HAPI_GET_NON_FUNGIBLE_TOKEN_INFO) - .withStatus(SUCCESS.getNumber()) + .withStatus(status.getNumber()) .withTokenInfo(tokenInfo) .withNftTokenInfo(nonFungibleTokenInfo) .build(); } public Bytes encodeGetTokenExpiryInfo(final TokenExpiryInfo tokenExpiryWrapper) { + return encodeGetTokenExpiryInfo(SUCCESS, tokenExpiryWrapper); + } + + public Bytes encodeGetTokenExpiryInfo(final ResponseCodeEnum status, final TokenExpiryInfo tokenExpiryWrapper) { return functionResultBuilder() .forFunction(FunctionType.HAPI_GET_TOKEN_EXPIRY_INFO) - .withStatus(SUCCESS.getNumber()) + .withStatus(status.getNumber()) .withExpiry(tokenExpiryWrapper) .build(); } public Bytes encodeGetTokenKey(final EvmKey keyValue) { + return encodeGetTokenKey(SUCCESS, keyValue); + } + + public Bytes encodeGetTokenKey(final ResponseCodeEnum status, final EvmKey keyValue) { return functionResultBuilder() .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) - .withStatus(SUCCESS.getNumber()) + .withStatus(status.getNumber()) .withKey(keyValue) .build(); } diff --git a/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/EvmNftInfo.java b/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/EvmNftInfo.java index 2763654c456c..485b8f2ccea3 100644 --- a/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/EvmNftInfo.java +++ b/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/EvmNftInfo.java @@ -29,7 +29,14 @@ public class EvmNftInfo { private Address spender; private byte[] ledgerId; - public EvmNftInfo() {} + public EvmNftInfo() { + this.serialNumber = 0L; + this.account = Address.ZERO; + this.creationTime = 0; + this.metadata = new byte[0]; + this.spender = Address.ZERO; + this.ledgerId = new byte[0]; + } public EvmNftInfo( long serialNumber, diff --git a/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/EvmTokenInfo.java b/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/EvmTokenInfo.java index 5d36e167c43f..4101479a128d 100644 --- a/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/EvmTokenInfo.java +++ b/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/EvmTokenInfo.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.evm.store.contracts.precompile.codec; +import java.util.ArrayList; import java.util.List; import org.hyperledger.besu.datatypes.Address; @@ -72,6 +73,29 @@ public EvmTokenInfo( this.expiry = expiry; } + public EvmTokenInfo() { + this.ledgerId = new byte[0]; + this.name = ""; + this.symbol = ""; + this.memo = ""; + this.treasury = Address.ZERO; + this.supplyType = 0; + this.deleted = false; + this.totalSupply = 0; + this.maxSupply = 0; + this.decimals = 0; + this.expiry = 0; + this.customFees = new ArrayList<>(); + this.adminKey = new EvmKey(); + this.kycKey = new EvmKey(); + this.freezeKey = new EvmKey(); + this.wipeKey = new EvmKey(); + this.supplyKey = new EvmKey(); + this.feeScheduleKey = new EvmKey(); + this.pauseKey = new EvmKey(); + this.autoRenewAccount = Address.ZERO; + } + public void setCustomFees(List customFees) { this.customFees = customFees; } diff --git a/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/TokenExpiryInfo.java b/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/TokenExpiryInfo.java index 38fa8b0d585c..2213453c7986 100644 --- a/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/TokenExpiryInfo.java +++ b/hedera-node/hedera-evm/src/main/java/com/hedera/node/app/service/evm/store/contracts/precompile/codec/TokenExpiryInfo.java @@ -30,6 +30,12 @@ public TokenExpiryInfo(long second, Address autoRenewAccount, long autoRenewPeri this.autoRenewPeriod = autoRenewPeriod; } + public TokenExpiryInfo() { + this.second = 0; + this.autoRenewAccount = Address.ZERO; + this.autoRenewPeriod = 0; + } + public long getSecond() { return second; } diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/AllowancePrecompile.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/AllowancePrecompile.java index 33cf9388996c..0074660df1a0 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/AllowancePrecompile.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/AllowancePrecompile.java @@ -43,6 +43,7 @@ import com.hedera.node.app.service.mono.store.contracts.precompile.codec.EncodingFacade; import com.hedera.node.app.service.mono.store.contracts.precompile.utils.PrecompilePricingUtils; import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.TokenID; import com.hederahashgraph.api.proto.java.TransactionBody; import java.util.Map; @@ -99,6 +100,11 @@ public Bytes getSuccessResultFor(final ExpirableTxnRecord.Builder childRecord) { : evmEncoder.encodeAllowance(value); } + @Override + public Bytes getFailureResultFor(final ResponseCodeEnum status) { + return tokenId == null ? encoder.encodeAllowance(status.getNumber(), 0) : evmEncoder.encodeAllowance(0); + } + public static TokenAllowanceWrapper decodeTokenAllowance( final Bytes input, final TokenID impliedTokenId, final UnaryOperator aliasResolver) { final var offset = impliedTokenId == null ? 1 : 0; diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/FungibleTokenInfoPrecompile.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/FungibleTokenInfoPrecompile.java index 3d094585ca25..2ee68c0a1545 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/FungibleTokenInfoPrecompile.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/FungibleTokenInfoPrecompile.java @@ -20,6 +20,7 @@ import static com.hedera.node.app.service.mono.store.contracts.precompile.codec.DecodingFacade.convertAddressBytesToTokenID; import com.hedera.node.app.service.evm.store.contracts.precompile.codec.EvmEncodingFacade; +import com.hedera.node.app.service.evm.store.contracts.precompile.codec.EvmTokenInfo; import com.hedera.node.app.service.evm.store.contracts.precompile.codec.TokenInfoWrapper; import com.hedera.node.app.service.evm.store.contracts.precompile.impl.EvmFungibleTokenInfoPrecompile; import com.hedera.node.app.service.mono.context.primitives.StateView; @@ -64,6 +65,12 @@ public Bytes getSuccessResultFor(final ExpirableTxnRecord.Builder childRecord) { return evmEncoder.encodeGetFungibleTokenInfo(tokenInfo); } + @Override + public Bytes getFailureResultFor(final ResponseCodeEnum status) { + final var tokenInfo = new EvmTokenInfo(); + return evmEncoder.encodeGetFungibleTokenInfo(status, tokenInfo); + } + public static TokenInfoWrapper decodeGetFungibleTokenInfo(final Bytes input) { final var rawTokenInfoWrapper = EvmFungibleTokenInfoPrecompile.decodeGetFungibleTokenInfo(input); return TokenInfoWrapper.forFungibleToken(convertAddressBytesToTokenID(rawTokenInfoWrapper.token())); diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetApprovedPrecompile.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetApprovedPrecompile.java index 02bfef2378a3..3da121d28967 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetApprovedPrecompile.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetApprovedPrecompile.java @@ -39,12 +39,14 @@ import com.hedera.node.app.service.mono.store.contracts.precompile.codec.EncodingFacade; import com.hedera.node.app.service.mono.store.contracts.precompile.utils.PrecompilePricingUtils; import com.hedera.node.app.service.mono.store.models.NftId; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.TokenID; import com.hederahashgraph.api.proto.java.TransactionBody; import java.math.BigInteger; import java.util.Objects; import java.util.function.UnaryOperator; import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Address; public class GetApprovedPrecompile extends AbstractReadOnlyPrecompile implements EvmGetApprovedPrecompile { @@ -94,6 +96,13 @@ public Bytes getSuccessResultFor(final ExpirableTxnRecord.Builder childRecord) { : evmEncoder.encodeGetApproved(canonicalSpender); } + @Override + public Bytes getFailureResultFor(final ResponseCodeEnum status) { + return tokenId == null + ? encoder.encodeGetApproved(status.getNumber(), Address.ZERO) + : evmEncoder.encodeGetApproved(Address.ZERO); + } + public static GetApprovedWrapper decodeGetApproved(final Bytes input, final TokenID impliedTokenId) { final var offset = impliedTokenId == null ? 1 : 0; if (offset == 1) { diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenDefaultFreezeStatus.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenDefaultFreezeStatus.java index e10d3048cdf2..e354b651ca47 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenDefaultFreezeStatus.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenDefaultFreezeStatus.java @@ -26,6 +26,7 @@ import com.hedera.node.app.service.mono.store.contracts.precompile.SyntheticTxnFactory; import com.hedera.node.app.service.mono.store.contracts.precompile.codec.EncodingFacade; import com.hedera.node.app.service.mono.store.contracts.precompile.utils.PrecompilePricingUtils; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.TokenID; import com.hederahashgraph.api.proto.java.TransactionBody; import java.util.Objects; @@ -60,6 +61,11 @@ public Bytes getSuccessResultFor(final ExpirableTxnRecord.Builder childRecord) { return evmEncoder.encodeGetTokenDefaultFreezeStatus(defaultFreezeStatus); } + @Override + public Bytes getFailureResultFor(final ResponseCodeEnum status) { + return evmEncoder.encodeGetTokenDefaultFreezeStatus(status, false); + } + public static GetTokenDefaultFreezeStatusWrapper decodeTokenDefaultFreezeStatus(final Bytes input) { final var rawGetTokenDefaultFreezeStatusWrapper = EvmGetTokenDefaultFreezeStatus.decodeTokenDefaultFreezeStatus(input); diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenDefaultKycStatus.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenDefaultKycStatus.java index fff3c9b7c6e0..8c100ffffae8 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenDefaultKycStatus.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenDefaultKycStatus.java @@ -26,6 +26,7 @@ import com.hedera.node.app.service.mono.store.contracts.precompile.SyntheticTxnFactory; import com.hedera.node.app.service.mono.store.contracts.precompile.codec.EncodingFacade; import com.hedera.node.app.service.mono.store.contracts.precompile.utils.PrecompilePricingUtils; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.TokenID; import com.hederahashgraph.api.proto.java.TransactionBody; import java.util.Objects; @@ -59,6 +60,11 @@ public Bytes getSuccessResultFor(final ExpirableTxnRecord.Builder childRecord) { return evmEncoder.encodeGetTokenDefaultKycStatus(defaultKycStatus); } + @Override + public Bytes getFailureResultFor(final ResponseCodeEnum status) { + return evmEncoder.encodeGetTokenDefaultKycStatus(status, false); + } + public static GetTokenDefaultKycStatusWrapper decodeTokenDefaultKycStatus(final Bytes input) { final var rawGetTokenDefaultKycStatusWrapper = EvmGetTokenDefaultKycStatus.decodeTokenDefaultKycStatus(input); return new GetTokenDefaultKycStatusWrapper<>( diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenExpiryInfoPrecompile.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenExpiryInfoPrecompile.java index af127d290463..20c03fcdea6e 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenExpiryInfoPrecompile.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenExpiryInfoPrecompile.java @@ -75,6 +75,12 @@ public Bytes getSuccessResultFor(final ExpirableTxnRecord.Builder childRecord) { return evmEncoder.encodeGetTokenExpiryInfo(expiryInfo); } + @Override + public Bytes getFailureResultFor(final ResponseCodeEnum status) { + final var tokenExpiry = new TokenExpiryInfo(); + return evmEncoder.encodeGetTokenExpiryInfo(status, tokenExpiry); + } + public static GetTokenExpiryInfoWrapper decodeGetTokenExpiryInfo(final Bytes input) { final var rawGetTokenExpityInfoWrapper = EvmGetTokenExpiryInfoPrecompile.decodeGetTokenExpiryInfo(input); return new GetTokenExpiryInfoWrapper<>(convertAddressBytesToTokenID(rawGetTokenExpityInfoWrapper.token())); diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenKeyPrecompile.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenKeyPrecompile.java index 9655777a1fb9..99066ea83bb0 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenKeyPrecompile.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenKeyPrecompile.java @@ -22,6 +22,7 @@ import static com.hedera.node.app.service.mono.utils.MiscUtils.asKeyUnchecked; import com.hedera.node.app.service.evm.store.contracts.precompile.codec.EvmEncodingFacade; +import com.hedera.node.app.service.evm.store.contracts.precompile.codec.EvmKey; import com.hedera.node.app.service.evm.store.contracts.precompile.codec.GetTokenKeyWrapper; import com.hedera.node.app.service.evm.store.contracts.precompile.impl.EvmGetTokenKeyPrecompile; import com.hedera.node.app.service.mono.ledger.properties.TokenProperty; @@ -69,6 +70,12 @@ public Bytes getSuccessResultFor(final ExpirableTxnRecord.Builder childRecord) { return evmEncoder.encodeGetTokenKey(evmKey); } + @Override + public Bytes getFailureResultFor(final ResponseCodeEnum status) { + final var evmKey = new EvmKey(); + return evmEncoder.encodeGetTokenKey(status, evmKey); + } + public static GetTokenKeyWrapper decodeGetTokenKey(final Bytes input) { final var rawGetTokenKeyWrapper = EvmGetTokenKeyPrecompile.decodeGetTokenKey(input); final var tokenID = convertAddressBytesToTokenID(rawGetTokenKeyWrapper.token()); diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenTypePrecompile.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenTypePrecompile.java index 4eb96d4fe6f9..000cf8cfe12f 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenTypePrecompile.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/GetTokenTypePrecompile.java @@ -62,6 +62,11 @@ public Bytes getSuccessResultFor(final ExpirableTxnRecord.Builder childRecord) { return evmEncoder.encodeGetTokenType(tokenType); } + @Override + public Bytes getFailureResultFor(final ResponseCodeEnum status) { + return evmEncoder.encodeGetTokenType(status, 0); + } + public static TokenInfoWrapper decodeGetTokenType(final Bytes input) { final var rawTokenInfoWrapper = EvmGetTokenTypePrecompile.decodeGetTokenType(input); return TokenInfoWrapper.forToken(convertAddressBytesToTokenID(rawTokenInfoWrapper.token())); diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsApprovedForAllPrecompile.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsApprovedForAllPrecompile.java index b5a1df3fcef3..db2aafc8d0c0 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsApprovedForAllPrecompile.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsApprovedForAllPrecompile.java @@ -39,6 +39,7 @@ import com.hedera.node.app.service.mono.store.contracts.precompile.codec.EncodingFacade; import com.hedera.node.app.service.mono.store.contracts.precompile.utils.PrecompilePricingUtils; import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.TokenID; import com.hederahashgraph.api.proto.java.TransactionBody; import java.util.Objects; @@ -101,6 +102,13 @@ public Bytes getSuccessResultFor(final ExpirableTxnRecord.Builder childRecord) { : evmEncoder.encodeIsApprovedForAll(answer); } + @Override + public Bytes getFailureResultFor(final ResponseCodeEnum status) { + return tokenId == null + ? encoder.encodeIsApprovedForAll(status.getNumber(), false) + : evmEncoder.encodeIsApprovedForAll(false); + } + public static IsApproveForAllWrapper decodeIsApprovedForAll( final Bytes input, final TokenID impliedTokenId, final UnaryOperator aliasResolver) { final var offset = impliedTokenId == null ? 1 : 0; diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsFrozenPrecompile.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsFrozenPrecompile.java index 20cf2a2ef784..96bec2521cde 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsFrozenPrecompile.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsFrozenPrecompile.java @@ -28,6 +28,7 @@ import com.hedera.node.app.service.mono.store.contracts.precompile.codec.EncodingFacade; import com.hedera.node.app.service.mono.store.contracts.precompile.utils.PrecompilePricingUtils; import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.TokenID; import com.hederahashgraph.api.proto.java.TransactionBody; import java.util.function.UnaryOperator; @@ -61,6 +62,11 @@ public Bytes getSuccessResultFor(final ExpirableTxnRecord.Builder childRecord) { return evmEncoder.encodeIsFrozen(isFrozen); } + @Override + public Bytes getFailureResultFor(final ResponseCodeEnum status) { + return evmEncoder.encodeIsFrozen(status, false); + } + public static TokenFreezeUnfreezeWrapper decodeIsFrozen( final Bytes input, final UnaryOperator aliasResolver) { final var rawTokenFreezeUnfreezeWrapper = EvmIsFrozenPrecompile.decodeIsFrozen(input); diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsKycPrecompile.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsKycPrecompile.java index f85ea57a05cf..8d4776494c43 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsKycPrecompile.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsKycPrecompile.java @@ -28,6 +28,7 @@ import com.hedera.node.app.service.mono.store.contracts.precompile.codec.EncodingFacade; import com.hedera.node.app.service.mono.store.contracts.precompile.utils.PrecompilePricingUtils; import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.TokenID; import com.hederahashgraph.api.proto.java.TransactionBody; import java.util.function.UnaryOperator; @@ -61,6 +62,11 @@ public Bytes getSuccessResultFor(final ExpirableTxnRecord.Builder childRecord) { return evmEncoder.encodeIsKyc(isKyc); } + @Override + public Bytes getFailureResultFor(final ResponseCodeEnum status) { + return evmEncoder.encodeIsKyc(status, false); + } + public static GrantRevokeKycWrapper decodeIsKyc( final Bytes input, final UnaryOperator aliasResolver) { final var rawGrantRevokeKycWrapper = EvmIsKycPrecompile.decodeIsKyc(input); diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsTokenPrecompile.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsTokenPrecompile.java index c9e6b321919c..5481aa6bd19d 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsTokenPrecompile.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/IsTokenPrecompile.java @@ -27,6 +27,7 @@ import com.hedera.node.app.service.mono.store.contracts.precompile.SyntheticTxnFactory; import com.hedera.node.app.service.mono.store.contracts.precompile.codec.EncodingFacade; import com.hedera.node.app.service.mono.store.contracts.precompile.utils.PrecompilePricingUtils; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.TokenID; import com.hederahashgraph.api.proto.java.TransactionBody; import java.util.function.UnaryOperator; @@ -58,6 +59,11 @@ public Bytes getSuccessResultFor(final ExpirableTxnRecord.Builder childRecord) { return evmEncoder.encodeIsToken(isToken); } + @Override + public Bytes getFailureResultFor(final ResponseCodeEnum status) { + return evmEncoder.encodeIsToken(status, false); + } + public static TokenInfoWrapper decodeIsToken(final Bytes input) { final var rawTokenInfoWrapper = EvmIsTokenPrecompile.decodeIsToken(input); return TokenInfoWrapper.forToken(convertAddressBytesToTokenID(rawTokenInfoWrapper.token())); diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/NonFungibleTokenInfoPrecompile.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/NonFungibleTokenInfoPrecompile.java index e2f87b4be447..cf30ce80c0c4 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/NonFungibleTokenInfoPrecompile.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/NonFungibleTokenInfoPrecompile.java @@ -20,6 +20,8 @@ import static com.hedera.node.app.service.mono.store.contracts.precompile.codec.DecodingFacade.convertAddressBytesToTokenID; import com.hedera.node.app.service.evm.store.contracts.precompile.codec.EvmEncodingFacade; +import com.hedera.node.app.service.evm.store.contracts.precompile.codec.EvmNftInfo; +import com.hedera.node.app.service.evm.store.contracts.precompile.codec.EvmTokenInfo; import com.hedera.node.app.service.evm.store.contracts.precompile.codec.TokenInfoWrapper; import com.hedera.node.app.service.evm.store.contracts.precompile.impl.EvmNonFungibleTokenInfoPrecompile; import com.hedera.node.app.service.mono.context.primitives.StateView; @@ -77,6 +79,13 @@ public Bytes getSuccessResultFor(final ExpirableTxnRecord.Builder childRecord) { return evmEncoder.encodeGetNonFungibleTokenInfo(tokenInfo, nonFungibleTokenInfo); } + @Override + public Bytes getFailureResultFor(final ResponseCodeEnum status) { + final var tokenInfo = new EvmTokenInfo(); + final var nonFungibleTokenInfo = new EvmNftInfo(); + return evmEncoder.encodeGetNonFungibleTokenInfo(status, tokenInfo, nonFungibleTokenInfo); + } + public static TokenInfoWrapper decodeGetNonFungibleTokenInfo(final Bytes input) { final var rawTokenInfoWrapper = EvmNonFungibleTokenInfoPrecompile.decodeGetNonFungibleTokenInfo(input); return TokenInfoWrapper.forNonFungibleToken( diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/TokenGetCustomFeesPrecompile.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/TokenGetCustomFeesPrecompile.java index 27e5fe3d99aa..05494f15062b 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/TokenGetCustomFeesPrecompile.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/TokenGetCustomFeesPrecompile.java @@ -19,6 +19,7 @@ import static com.hedera.node.app.service.evm.utils.ValidationUtils.validateTrue; import static com.hedera.node.app.service.mono.store.contracts.precompile.codec.DecodingFacade.convertAddressBytesToTokenID; +import com.hedera.node.app.service.evm.store.contracts.precompile.codec.CustomFee; import com.hedera.node.app.service.evm.store.contracts.precompile.codec.EvmEncodingFacade; import com.hedera.node.app.service.evm.store.contracts.precompile.codec.TokenGetCustomFeesWrapper; import com.hedera.node.app.service.evm.store.contracts.precompile.impl.EvmTokenGetCustomFeesPrecompile; @@ -30,6 +31,7 @@ import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.TokenID; import com.hederahashgraph.api.proto.java.TransactionBody.Builder; +import java.util.ArrayList; import java.util.function.UnaryOperator; import org.apache.tuweni.bytes.Bytes; @@ -61,6 +63,12 @@ public Bytes getSuccessResultFor(final ExpirableTxnRecord.Builder childRecord) { return evmEncoder.encodeTokenGetCustomFees(customFees); } + @Override + public Bytes getFailureResultFor(final ResponseCodeEnum status) { + final var customFees = new ArrayList(); + return evmEncoder.encodeTokenGetCustomFees(status, customFees); + } + public static TokenGetCustomFeesWrapper decodeTokenGetCustomFees(final Bytes input) { final var rawTokenGetCustomFeesWrapper = EvmTokenGetCustomFeesPrecompile.decodeTokenGetCustomFees(input); return new TokenGetCustomFeesWrapper<>(convertAddressBytesToTokenID(rawTokenGetCustomFeesWrapper.token())); diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/TokenInfoPrecompile.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/TokenInfoPrecompile.java index c3ec037bda22..681b45948444 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/TokenInfoPrecompile.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/contracts/precompile/impl/TokenInfoPrecompile.java @@ -20,6 +20,7 @@ import static com.hedera.node.app.service.mono.store.contracts.precompile.codec.DecodingFacade.convertAddressBytesToTokenID; import com.hedera.node.app.service.evm.store.contracts.precompile.codec.EvmEncodingFacade; +import com.hedera.node.app.service.evm.store.contracts.precompile.codec.EvmTokenInfo; import com.hedera.node.app.service.evm.store.contracts.precompile.codec.TokenInfoWrapper; import com.hedera.node.app.service.evm.store.contracts.precompile.impl.EvmTokenInfoPrecompile; import com.hedera.node.app.service.mono.context.primitives.StateView; @@ -64,6 +65,12 @@ public Bytes getSuccessResultFor(final ExpirableTxnRecord.Builder childRecord) { return evmEncoder.encodeGetTokenInfo(tokenInfo); } + @Override + public Bytes getFailureResultFor(final ResponseCodeEnum status) { + final var tokenInfo = new EvmTokenInfo(); + return evmEncoder.encodeGetTokenInfo(status, tokenInfo); + } + public static TokenInfoWrapper decodeGetTokenInfo(final Bytes input) { final var rawTokenInfoWrapper = EvmTokenInfoPrecompile.decodeGetTokenInfo(input); return TokenInfoWrapper.forToken(convertAddressBytesToTokenID(rawTokenInfoWrapper.token())); diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/tokens/HederaTokenStore.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/tokens/HederaTokenStore.java index b824a5574069..efcc361463c4 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/tokens/HederaTokenStore.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/tokens/HederaTokenStore.java @@ -371,6 +371,7 @@ public ResponseCodeEnum changeOwnerWildCard(final NftId nftId, final AccountID f @Override public boolean matchesTokenDecimals(final TokenID tId, final int expectedDecimals) { + // Note: this method assumes that the token for tId exists! Otherwise get(tId) will throw an exception return get(tId).decimals() == expectedDecimals; } diff --git a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/tokens/TokenStore.java b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/tokens/TokenStore.java index aac13b6fe6ef..b7952f65a6ac 100644 --- a/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/tokens/TokenStore.java +++ b/hedera-node/hedera-mono-service/src/main/java/com/hedera/node/app/service/mono/store/tokens/TokenStore.java @@ -83,22 +83,22 @@ default ResponseCodeEnum delete(TokenID id) { } default ResponseCodeEnum tryTokenChange(BalanceChange change) { - var validity = OK; var tokenId = resolve(change.tokenId()); if (tokenId == MISSING_TOKEN) { - validity = INVALID_TOKEN_ID; + return INVALID_TOKEN_ID; } + if (change.hasExpectedDecimals() && !matchesTokenDecimals(change.tokenId(), change.getExpectedDecimals())) { - validity = UNEXPECTED_TOKEN_DECIMALS; + return UNEXPECTED_TOKEN_DECIMALS; } - if (validity == OK) { - if (change.isForNft()) { - validity = changeOwner(change.nftId(), change.accountId(), change.counterPartyAccountId()); - } else { - validity = adjustBalance(change.accountId(), tokenId, change.getAggregatedUnits()); - if (validity == INSUFFICIENT_TOKEN_BALANCE) { - validity = change.codeForInsufficientBalance(); - } + + var validity = OK; + if (change.isForNft()) { + validity = changeOwner(change.nftId(), change.accountId(), change.counterPartyAccountId()); + } else { + validity = adjustBalance(change.accountId(), tokenId, change.getAggregatedUnits()); + if (validity == INSUFFICIENT_TOKEN_BALANCE) { + validity = change.codeForInsufficientBalance(); } } return validity; diff --git a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/GetTokenExpiryInfoPrecompileTest.java b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/GetTokenExpiryInfoPrecompileTest.java index 53c9f46830d3..a139a3117393 100644 --- a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/GetTokenExpiryInfoPrecompileTest.java +++ b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/GetTokenExpiryInfoPrecompileTest.java @@ -276,6 +276,7 @@ void getTokenExpiryInfoFails() { subject.prepareFields(frame); subject.prepareComputation(pretendArguments, a -> a); subject.getPrecompile().getGasRequirement(TEST_CONSENSUS_TIME); + given(evmEncoder.encodeGetTokenExpiryInfo(any(), any())).willReturn(invalidTokenIdResult); final var result = subject.computeInternal(frame); // then: diff --git a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/GetTokenInfoPrecompilesTest.java b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/GetTokenInfoPrecompilesTest.java index 3258f04a9f0a..eb76d3c78921 100644 --- a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/GetTokenInfoPrecompilesTest.java +++ b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/GetTokenInfoPrecompilesTest.java @@ -757,6 +757,7 @@ void getTokenInfoOfMissingTokenFails() { subject.prepareFields(frame); subject.prepareComputation(pretendArguments, a -> a); subject.getPrecompile().getGasRequirement(TEST_CONSENSUS_TIME); + given(evmEncoder.encodeGetTokenInfo(any(), any())).willReturn(invalidTokenIdResult); final var result = subject.computeInternal(frame); // then: @@ -789,6 +790,7 @@ void getFungibleTokenInfoOfMissingTokenFails() { subject.prepareFields(frame); subject.prepareComputation(pretendArguments, a -> a); subject.getPrecompile().getGasRequirement(TEST_CONSENSUS_TIME); + given(evmEncoder.encodeGetFungibleTokenInfo(any(), any())).willReturn(invalidTokenIdResult); final var result = subject.computeInternal(frame); // then: @@ -826,6 +828,7 @@ void getNonFungibleTokenInfoOfMissingSerialNumberFails() { subject.prepareFields(frame); subject.prepareComputation(pretendArguments, a -> a); subject.getPrecompile().getGasRequirement(TEST_CONSENSUS_TIME); + given(evmEncoder.encodeGetNonFungibleTokenInfo(any(), any(), any())).willReturn(invalidSerialNumberResult); final var result = subject.computeInternal(frame); // then: diff --git a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/GetTokenKeyPrecompileTest.java b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/GetTokenKeyPrecompileTest.java index 33723c50edfa..41a89578ea67 100644 --- a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/GetTokenKeyPrecompileTest.java +++ b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/GetTokenKeyPrecompileTest.java @@ -18,6 +18,7 @@ import static com.hedera.node.app.service.evm.store.contracts.precompile.codec.TokenKeyType.ADMIN_KEY; import static com.hedera.node.app.service.mono.store.contracts.precompile.HTSTestsUtil.contractAddress; +import static com.hedera.node.app.service.mono.store.contracts.precompile.HTSTestsUtil.failResult; import static com.hedera.node.app.service.mono.store.contracts.precompile.HTSTestsUtil.fungible; import static com.hedera.node.app.service.mono.store.contracts.precompile.HTSTestsUtil.invalidTokenIdResult; import static com.hedera.node.app.service.mono.store.contracts.precompile.HTSTestsUtil.successResult; @@ -345,6 +346,7 @@ void callForGetFungibleTokenKeyWithInvalidKeyFails() { // when subject.prepareFields(frame); subject.prepareComputation(input, a -> a); + given(evmEncoder.encodeGetTokenKey(any(), any())).willReturn(failResult); final var result = subject.computeInternal(frame); // then assertEquals(HTSTestsUtil.failResult, result); @@ -363,6 +365,7 @@ void getTokenKeyCallForInvalidTokenIds() { // when subject.prepareFields(frame); subject.prepareComputation(input, a -> a); + given(evmEncoder.encodeGetTokenKey(any(), any())).willReturn(invalidTokenIdResult); final var result = subject.computeInternal(frame); // then assertEquals(invalidTokenIdResult, result); diff --git a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/TokenGetCustomFeesPrecompileTest.java b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/TokenGetCustomFeesPrecompileTest.java index a4a0731e9f4c..3474e147e2ea 100644 --- a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/TokenGetCustomFeesPrecompileTest.java +++ b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/contracts/precompile/TokenGetCustomFeesPrecompileTest.java @@ -259,6 +259,7 @@ void getTokenCustomFeesMissingTokenIdFails() { subject.prepareFields(frame); subject.prepareComputation(pretendArguments, a -> a); subject.getPrecompile().getGasRequirement(TEST_CONSENSUS_TIME); + given(evmEncoder.encodeTokenGetCustomFees(any(), any())).willReturn(invalidTokenIdResult); final var result = subject.computeInternal(frame); // then: diff --git a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/tokens/HederaTokenStoreTest.java b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/tokens/HederaTokenStoreTest.java index e30714a29fa9..4fcf68adec59 100644 --- a/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/tokens/HederaTokenStoreTest.java +++ b/hedera-node/hedera-mono-service/src/test/java/com/hedera/node/app/service/mono/store/tokens/HederaTokenStoreTest.java @@ -28,12 +28,8 @@ import static com.hedera.node.app.service.mono.ledger.properties.TokenRelProperty.IS_KYC_GRANTED; import static com.hedera.node.app.service.mono.ledger.properties.TokenRelProperty.TOKEN_BALANCE; import static com.hedera.node.app.service.mono.state.submerkle.EntityId.MISSING_ENTITY_ID; -import static com.hedera.test.factories.scenarios.TxnHandlingScenario.COMPLEX_KEY_ACCOUNT_KT; -import static com.hedera.test.factories.scenarios.TxnHandlingScenario.MISC_ACCOUNT_KT; import static com.hedera.test.factories.scenarios.TxnHandlingScenario.TOKEN_ADMIN_KT; import static com.hedera.test.factories.scenarios.TxnHandlingScenario.TOKEN_FEE_SCHEDULE_KT; -import static com.hedera.test.factories.scenarios.TxnHandlingScenario.TOKEN_FREEZE_KT; -import static com.hedera.test.factories.scenarios.TxnHandlingScenario.TOKEN_KYC_KT; import static com.hedera.test.factories.scenarios.TxnHandlingScenario.TOKEN_PAUSE_KT; import static com.hedera.test.factories.scenarios.TxnHandlingScenario.TOKEN_TREASURY_KT; import static com.hedera.test.mocks.TestContextValidator.CONSENSUS_NOW; @@ -105,8 +101,6 @@ import com.hedera.node.app.service.mono.state.validation.UsageLimits; import com.hedera.node.app.service.mono.store.models.Id; import com.hedera.node.app.service.mono.store.models.NftId; -import com.hedera.node.app.service.mono.utils.EntityNumPair; -import com.hedera.node.app.service.mono.utils.NftNumPair; import com.hedera.test.factories.scenarios.TxnHandlingScenario; import com.hedera.test.utils.IdUtils; import com.hederahashgraph.api.proto.java.AccountAmount; @@ -115,13 +109,10 @@ import com.hederahashgraph.api.proto.java.Key; import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.Timestamp; -import com.hederahashgraph.api.proto.java.TokenCreateTransactionBody; import com.hederahashgraph.api.proto.java.TokenID; import com.hederahashgraph.api.proto.java.TokenUpdateTransactionBody; import java.util.EnumSet; -import java.util.HashSet; import java.util.Optional; -import java.util.Set; import java.util.function.Consumer; import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.Assertions; @@ -132,18 +123,12 @@ class HederaTokenStoreTest { private static final Key newKey = TxnHandlingScenario.TOKEN_REPLACE_KT.asKey(); private static final JKey newFcKey = TxnHandlingScenario.TOKEN_REPLACE_KT.asJKeyUnchecked(); - private static final Key adminKey = TOKEN_ADMIN_KT.asKey(); - private static final Key kycKey = TOKEN_KYC_KT.asKey(); - private static final Key freezeKey = TOKEN_FREEZE_KT.asKey(); - private static final Key wipeKey = MISC_ACCOUNT_KT.asKey(); - private static final Key supplyKey = COMPLEX_KEY_ACCOUNT_KT.asKey(); private static final Key feeScheduleKey = TOKEN_FEE_SCHEDULE_KT.asKey(); private static final Key pauseKey = TOKEN_PAUSE_KT.asKey(); private static final String symbol = "NOTHBAR"; private static final String newSymbol = "REALLYSOM"; private static final String newMemo = "NEWMEMO"; - private static final String memo = "TOKENMEMO"; private static final String name = "TOKENNAME"; private static final String newName = "NEWNAME"; private static final int maxCustomFees = 5; @@ -151,15 +136,12 @@ class HederaTokenStoreTest { private static final int numPositiveBalances = 1; private static final long expiry = CONSENSUS_NOW + 1_234_567L; private static final long newExpiry = CONSENSUS_NOW + 1_432_765L; - private static final long totalSupply = 1_000_000L; - private static final int decimals = 10; private static final long treasuryBalance = 50_000L; private static final long sponsorBalance = 1_000L; private static final TokenID misc = IdUtils.asToken("0.0.1"); private static final TokenID nonfungible = IdUtils.asToken("0.0.2"); private static final int maxAutoAssociations = 1234; private static final int alreadyUsedAutoAssocitaions = 123; - private static final boolean freezeDefault = true; private static final long newAutoRenewPeriod = 2_000_000L; private static final AccountID payer = IdUtils.asAccount("0.0.12345"); private static final AccountID autoRenewAccount = IdUtils.asAccount("0.0.5"); @@ -682,12 +664,6 @@ void changingOwnerDoesTheExpected() { final long startSponsorANfts = 4; final long startCounterpartyANfts = 1; final var receiver = EntityId.fromGrpcAccountId(counterparty); - final var nftNumPair1 = NftNumPair.fromLongs(1111, 111); - final var nftId1 = nftNumPair1.nftId(); - final var nftNumPair2 = NftNumPair.fromLongs(1112, 112); - final var nftId2 = nftNumPair2.nftId(); - final var nftNumPair3 = NftNumPair.fromLongs(1113, 113); - final var nftId3 = nftNumPair3.nftId(); given(accountsLedger.get(sponsor, NUM_NFTS_OWNED)).willReturn(startSponsorNfts); given(accountsLedger.get(counterparty, NUM_NFTS_OWNED)).willReturn(startCounterpartyNfts); given(tokenRelsLedger.get(sponsorNft, TOKEN_BALANCE)).willReturn(startSponsorANfts); @@ -716,9 +692,7 @@ void changingOwnerDoesTheExpectedWithTreasuryReturn() { final long startCounterpartyNfts = 8; final long startTreasuryTNfts = 4; final long startCounterpartyTNfts = 1; - final var sender = EntityId.fromGrpcAccountId(counterparty); final var receiver = EntityId.fromGrpcAccountId(primaryTreasury); - final var muti = EntityNumPair.fromLongs(tNft.tokenId().getTokenNum(), tNft.serialNo()); given(backingTokens.getImmutableRef(tNft.tokenId()).treasury()).willReturn(receiver); given(accountsLedger.get(primaryTreasury, NUM_NFTS_OWNED)).willReturn(startTreasuryNfts); given(accountsLedger.get(counterparty, NUM_NFTS_OWNED)).willReturn(startCounterpartyNfts); @@ -751,8 +725,6 @@ void changingOwnerDoesTheExpectedWithTreasuryExit() { final long startCounterpartyTNfts = 1; final var sender = EntityId.fromGrpcAccountId(primaryTreasury); final var receiver = EntityId.fromGrpcAccountId(counterparty); - final var nftNumPair3 = NftNumPair.fromLongs(1113, 113); - final var nftId3 = nftNumPair3.nftId(); given(accountsLedger.get(primaryTreasury, NUM_NFTS_OWNED)).willReturn(startTreasuryNfts); given(accountsLedger.get(counterparty, NUM_NFTS_OWNED)).willReturn(startCounterpartyNfts); given(tokenRelsLedger.get(treasuryNft, TOKEN_BALANCE)).willReturn(startTreasuryTNfts); @@ -986,8 +958,6 @@ void updateRejectsInappropriateSupplyKey() { @Test void updateRejectsZeroTokenBalanceKey() { - final Set tokenSet = new HashSet<>(); - tokenSet.add(nonfungible); givenUpdateTarget(ALL_KEYS, nonfungibleToken); final var op = updateWith(ALL_KEYS, nonfungible, true, true, true).toBuilder() .setExpiry(Timestamp.newBuilder().setSeconds(0)) @@ -1000,8 +970,6 @@ void updateRejectsZeroTokenBalanceKey() { @Test void updateHappyPathIgnoresZeroExpiry() { - final Set tokenSet = new HashSet<>(); - tokenSet.add(misc); givenUpdateTarget(ALL_KEYS, token); final var op = updateWith(ALL_KEYS, misc, true, true, true).toBuilder() .setExpiry(Timestamp.newBuilder().setSeconds(0)) @@ -1447,6 +1415,23 @@ void failsIfMismatchingDecimals() { Assertions.assertEquals(UNEXPECTED_TOKEN_DECIMALS, result); } + @Test + void invalidTokenTakesPriorityOverDecimalCheck() { + final var aa = + AccountAmount.newBuilder().setAccountID(sponsor).setAmount(100).build(); + final var fungibleChange = BalanceChange.changingFtUnits(Id.fromGrpcToken(misc), misc, aa, payer); + assertFalse(fungibleChange.hasExpectedDecimals()); + + // Here we set the expected decimals in the change, but also simulate the token not existing + fungibleChange.setExpectedDecimals(4); + given(backingTokens.contains(misc)).willReturn(false); + + final var result = subject.tryTokenChange(fungibleChange); + // Even though the expected decimals don't match, the invalid token status takes priority (i.e. is returned + // instead of a decimal error status) + Assertions.assertEquals(INVALID_TOKEN_ID, result); + } + @Test void decimalMatchingWorks() { assertEquals(2, subject.get(misc).decimals()); @@ -1509,27 +1494,9 @@ void updateExpiryInfoRejectsMissingToken() { assertEquals(INVALID_TOKEN_ID, outcome); } - TokenCreateTransactionBody.Builder fullyValidTokenCreateAttempt() { - return TokenCreateTransactionBody.newBuilder() - .setExpiry(Timestamp.newBuilder().setSeconds(expiry)) - .setMemo(memo) - .setAdminKey(adminKey) - .setKycKey(kycKey) - .setFreezeKey(freezeKey) - .setWipeKey(wipeKey) - .setSupplyKey(supplyKey) - .setFeeScheduleKey(feeScheduleKey) - .setSymbol(symbol) - .setName(name) - .setInitialSupply(totalSupply) - .setTreasury(treasury) - .setDecimals(decimals) - .setFreezeDefault(freezeDefault); - } - private void assertSoleTokenChangesAreForNftTransfer(final NftId nft, final AccountID from, final AccountID to) { final var tokenChanges = sideEffectsTracker.getNetTrackedTokenUnitAndOwnershipChanges(); - final var ownershipChange = tokenChanges.get(0); + final var ownershipChange = tokenChanges.getFirst(); assertEquals(nft.tokenId(), ownershipChange.getToken()); final var nftTransfer = ownershipChange.getNftTransfers(0); assertEquals(nft.serialNo(), nftTransfer.getSerialNumber()); diff --git a/hedera-node/hedera-mono-service/src/test/resources/testfiles/config.txt b/hedera-node/hedera-mono-service/src/test/resources/testfiles/config.txt index c07b64999488..c1f52c1cd832 100644 --- a/hedera-node/hedera-mono-service/src/test/resources/testfiles/config.txt +++ b/hedera-node/hedera-mono-service/src/test/resources/testfiles/config.txt @@ -63,12 +63,6 @@ TLS, on #on to enable TLS, off to disable. # # FilesystemDemo.jar parameters: none # -# HashGraphDemo.jar takes parameters that give the initial checkbox settings, -# in the same order they appear are on the screen, with 1 to check it -# and 0 to not check it, followed by the number of events to display -# (or “all”). The first parameter controls whether it runs -# slowly (1) or runs at full speed (0). -# # GameDemo.jar parameters: # height: height of the board (in cells). Player moves 1 cell at a time. # width: width of the board (in cells). Player moves 1 cell at a time. 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..f6dc16a012ac 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 @@ -126,8 +126,13 @@ public Transaction syntheticTransactionForHtsCall(Bytes input, ContractID contra if (isViewCall) { contractCallBodyBuilder.gas(1L); } + final var parentTxBody = context.body(); var transactionBody = TransactionBody.newBuilder() + .nodeAccountID(parentTxBody.nodeAccountID()) .transactionID(TransactionID.DEFAULT) + .transactionFee(parentTxBody.transactionFee()) + .transactionValidDuration(parentTxBody.transactionValidDuration()) + .memo(parentTxBody.memo()) .contractCall(contractCallBodyBuilder.build()) .build(); return transactionWith(transactionBody); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/HtsSystemContract.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/HtsSystemContract.java index 06695f60b626..1cfac03152a8 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/HtsSystemContract.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/HtsSystemContract.java @@ -105,7 +105,7 @@ private static FullResult resultOfExecuting( if (pricedResult.isViewCall()) { final var proxyWorldUpdater = FrameUtils.proxyUpdaterFor(frame); final var enhancement = proxyWorldUpdater.enhancement(); - final var responseCode = pricedResult.responseCode() != null ? pricedResult.responseCode() : null; + final var responseCode = pricedResult.responseCode(); if (responseCode == SUCCESS) { enhancement diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/AbstractNftViewCall.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/AbstractNftViewCall.java index 3bce2839701c..772c87b9c6c0 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/AbstractNftViewCall.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/AbstractNftViewCall.java @@ -17,14 +17,19 @@ package com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_NFT_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; +import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.FullResult.haltResult; import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.FullResult.revertResult; +import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCall.PricedResult.gasOnly; import static java.util.Objects.requireNonNull; +import com.hedera.hapi.node.base.TokenType; import com.hedera.hapi.node.state.token.Nft; import com.hedera.hapi.node.state.token.Token; import com.hedera.node.app.service.contract.impl.exec.gas.SystemContractGasCalculator; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.FullResult; import com.hedera.node.app.service.contract.impl.hevm.HederaWorldUpdater; +import com.hedera.node.app.service.evm.contracts.operations.HederaExceptionalHaltReason; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -43,6 +48,21 @@ protected AbstractNftViewCall( this.serialNo = serialNo; } + @Override + public @NonNull PricedResult execute() { + if (token != null && token.tokenType() == TokenType.FUNGIBLE_COMMON) { + // (FUTURE) consider removing this pattern, but for now match + // mono-service by halting on invalid token type + return gasOnly( + haltResult( + HederaExceptionalHaltReason.ERROR_DECODING_PRECOMPILE_INPUT, + gasCalculator.viewGasRequirement()), + INVALID_TOKEN_ID, + false); + } + return super.execute(); + } + /** * {@inheritDoc} */ diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/decimals/DecimalsCall.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/decimals/DecimalsCall.java index 73ace233069a..0c91baeabe9d 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/decimals/DecimalsCall.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/decimals/DecimalsCall.java @@ -17,8 +17,9 @@ package com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.decimals; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; -import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.FullResult.revertResult; +import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.FullResult.haltResult; import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.FullResult.successResult; +import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCall.PricedResult.gasOnly; import com.hedera.hapi.node.base.TokenType; import com.hedera.hapi.node.state.token.Token; @@ -26,6 +27,7 @@ import com.hedera.node.app.service.contract.impl.exec.systemcontracts.FullResult; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.AbstractRevertibleTokenViewCall; import com.hedera.node.app.service.contract.impl.hevm.HederaWorldUpdater; +import com.hedera.node.app.service.evm.contracts.operations.HederaExceptionalHaltReason; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -42,18 +44,28 @@ public DecimalsCall( super(gasCalculator, enhancement, token); } + @Override + public @NonNull PricedResult execute() { + if (token != null && token.tokenType() != TokenType.FUNGIBLE_COMMON) { + // (FUTURE) consider removing this pattern, but for now match + // mono-service by halting on invalid token type + return gasOnly( + haltResult( + HederaExceptionalHaltReason.ERROR_DECODING_PRECOMPILE_INPUT, + gasCalculator.viewGasRequirement()), + INVALID_TOKEN_ID, + false); + } + return super.execute(); + } + /** * {@inheritDoc} */ @Override protected @NonNull FullResult resultOfViewingToken(@NonNull final Token token) { - if (token.tokenType() != TokenType.FUNGIBLE_COMMON) { - return revertResult(INVALID_TOKEN_ID, gasCalculator.viewGasRequirement()); - } else { - final var decimals = Math.min(MAX_REPORTABLE_DECIMALS, token.decimals()); - return successResult( - DecimalsTranslator.DECIMALS.getOutputs().encodeElements(decimals), - gasCalculator.viewGasRequirement()); - } + final var decimals = Math.min(MAX_REPORTABLE_DECIMALS, token.decimals()); + return successResult( + DecimalsTranslator.DECIMALS.getOutputs().encodeElements(decimals), gasCalculator.viewGasRequirement()); } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/tokenuri/TokenUriCall.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/tokenuri/TokenUriCall.java index 0c3df42ec93b..ff8423cf2232 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/tokenuri/TokenUriCall.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/tokenuri/TokenUriCall.java @@ -16,12 +16,9 @@ package com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.tokenuri; -import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.FullResult.revertResult; import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.FullResult.successResult; import static java.util.Objects.requireNonNull; -import com.hedera.hapi.node.base.ResponseCodeEnum; -import com.hedera.hapi.node.base.TokenType; import com.hedera.hapi.node.state.token.Nft; import com.hedera.hapi.node.state.token.Token; import com.hedera.node.app.service.contract.impl.exec.gas.SystemContractGasCalculator; @@ -51,12 +48,6 @@ public TokenUriCall( */ @Override protected @NonNull FullResult resultOfViewingNft(@NonNull final Token token, final Nft nft) { - requireNonNull(token); - // #10568 - We add this check to match mono behavior - if (token.tokenType() == TokenType.FUNGIBLE_COMMON) { - return revertResult(ResponseCodeEnum.INVALID_TOKEN_ID, gasCalculator.viewGasRequirement()); - } - String metadata; if (nft != null) { metadata = new String(nft.metadata().toByteArray()); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleSystemContractOperationsTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleSystemContractOperationsTest.java index c596aee56865..446a8145c748 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleSystemContractOperationsTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleSystemContractOperationsTest.java @@ -194,6 +194,7 @@ void externalizeFailedResultTest() { @Test void syntheticTransactionForHtsCallTest() { + given(context.body()).willReturn(TransactionBody.DEFAULT); assertNotNull(subject.syntheticTransactionForHtsCall(Bytes.EMPTY, ContractID.DEFAULT, true)); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/decimals/DecimalsCallTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/decimals/DecimalsCallTest.java index f39997d31e81..6e307f9bf5a8 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/decimals/DecimalsCallTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/decimals/DecimalsCallTest.java @@ -16,16 +16,15 @@ package com.hedera.node.app.service.contract.impl.test.exec.systemcontracts.hts.decimals; -import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.FUNGIBLE_TOKEN; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.NON_FUNGIBLE_TOKEN; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.UNREASONABLY_DIVISIBLE_TOKEN; -import static com.hedera.node.app.service.contract.impl.test.TestHelpers.revertOutputFor; import static org.junit.jupiter.api.Assertions.*; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.decimals.DecimalsCall; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.decimals.DecimalsTranslator; import com.hedera.node.app.service.contract.impl.test.exec.systemcontracts.hts.HtsCallTestBase; +import com.hedera.node.app.service.evm.contracts.operations.HederaExceptionalHaltReason; import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.evm.frame.MessageFrame; import org.junit.jupiter.api.Test; @@ -35,13 +34,15 @@ class DecimalsCallTest extends HtsCallTestBase { private DecimalsCall subject; @Test - void revertsWithNonfungibleToken() { + void haltWithNonfungibleToken() { subject = new DecimalsCall(mockEnhancement(), gasCalculator, NON_FUNGIBLE_TOKEN); final var result = subject.execute().fullResult().result(); - assertEquals(MessageFrame.State.REVERT, result.getState()); - assertEquals(revertOutputFor(INVALID_TOKEN_ID), result.getOutput()); + assertEquals(MessageFrame.State.EXCEPTIONAL_HALT, result.getState()); + assertEquals( + HederaExceptionalHaltReason.ERROR_DECODING_PRECOMPILE_INPUT, + result.getHaltReason().get()); } @Test diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/ownerof/OwnerOfCallTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/ownerof/OwnerOfCallTest.java index e89d44a5abcd..8ce5df95dcd6 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/ownerof/OwnerOfCallTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/ownerof/OwnerOfCallTest.java @@ -21,6 +21,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.ALIASED_SOMEBODY; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.CIVILIAN_OWNED_NFT; +import static com.hedera.node.app.service.contract.impl.test.TestHelpers.FUNGIBLE_TOKEN; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.NFT_SERIAL_NO; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.NON_FUNGIBLE_TOKEN; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.NON_FUNGIBLE_TOKEN_ID; @@ -35,6 +36,7 @@ import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.ownerof.OwnerOfTranslator; import com.hedera.node.app.service.contract.impl.test.TestHelpers; import com.hedera.node.app.service.contract.impl.test.exec.systemcontracts.hts.HtsCallTestBase; +import com.hedera.node.app.service.evm.contracts.operations.HederaExceptionalHaltReason; import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.evm.frame.MessageFrame; import org.junit.jupiter.api.Test; @@ -63,6 +65,21 @@ void revertsWithMissingNft() { assertEquals(revertOutputFor(INVALID_NFT_ID), result.getOutput()); } + @Test + void haltWhenTokenIsNotERC721() { + // given + subject = new OwnerOfCall(gasCalculator, mockEnhancement(), FUNGIBLE_TOKEN, NFT_SERIAL_NO); + + // when + final var result = subject.execute().fullResult().result(); + + // then + assertEquals(MessageFrame.State.EXCEPTIONAL_HALT, result.getState()); + assertEquals( + HederaExceptionalHaltReason.ERROR_DECODING_PRECOMPILE_INPUT, + result.getHaltReason().get()); + } + @Test void revertsWithMissingOwner() { subject = new OwnerOfCall(gasCalculator, mockEnhancement(), NON_FUNGIBLE_TOKEN, NFT_SERIAL_NO); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/tokenuri/TokenUriCallTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/tokenuri/TokenUriCallTest.java index 5de3a834c130..d641a87bf4af 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/tokenuri/TokenUriCallTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/tokenuri/TokenUriCallTest.java @@ -24,10 +24,10 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.given; -import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.tokenuri.TokenUriCall; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.tokenuri.TokenUriTranslator; import com.hedera.node.app.service.contract.impl.test.exec.systemcontracts.hts.HtsCallTestBase; +import com.hedera.node.app.service.evm.contracts.operations.HederaExceptionalHaltReason; import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.evm.frame.MessageFrame; import org.junit.jupiter.api.Test; @@ -54,17 +54,35 @@ void returnsUnaliasedOwnerLongZeroForPresentTokenAndNonTreasuryNft() { } @Test - void revertsWhenTokenIsNotERC721() { + void returnNonExistingTokenErrorMetadata() { // given - subject = new TokenUriCall(gasCalculator, mockEnhancement(), FUNGIBLE_TOKEN, NFT_SERIAL_NO); - given(nativeOperations.getNft(FUNGIBLE_TOKEN.tokenId().tokenNum(), NFT_SERIAL_NO)) + subject = new TokenUriCall(gasCalculator, mockEnhancement(), NON_FUNGIBLE_TOKEN, NFT_SERIAL_NO); + given(nativeOperations.getNft(NON_FUNGIBLE_TOKEN.tokenId().tokenNum(), NFT_SERIAL_NO)) .willReturn(null); + // when + final var result = subject.execute().fullResult().result(); + // then + assertEquals(MessageFrame.State.COMPLETED_SUCCESS, result.getState()); + assertEquals( + Bytes.wrap(TokenUriTranslator.TOKEN_URI + .getOutputs() + .encodeElements(TokenUriCall.URI_QUERY_NON_EXISTING_TOKEN_ERROR) + .array()), + result.getOutput()); + } + + @Test + void haltWhenTokenIsNotERC721() { + // given + subject = new TokenUriCall(gasCalculator, mockEnhancement(), FUNGIBLE_TOKEN, NFT_SERIAL_NO); // when final var result = subject.execute().fullResult().result(); // then - assertEquals(MessageFrame.State.REVERT, result.getState()); - assertEquals(Bytes.wrap(ResponseCodeEnum.INVALID_TOKEN_ID.name().getBytes()), result.getOutput()); + assertEquals(MessageFrame.State.EXCEPTIONAL_HALT, result.getState()); + assertEquals( + HederaExceptionalHaltReason.ERROR_DECODING_PRECOMPILE_INPUT, + result.getHaltReason().get()); } } diff --git a/hedera-node/test-clients/rcdiff/check.sh b/hedera-node/test-clients/rcdiff/check.sh index e2badc16bd04..c4cf6ec122b5 100755 --- a/hedera-node/test-clients/rcdiff/check.sh +++ b/hedera-node/test-clients/rcdiff/check.sh @@ -10,4 +10,5 @@ else fi # Change -m value to limit the number of diffs written to file -java -jar rcdiff.jar -e $EXPECTED_LOC -a $ACTUAL_LOC -m 1000 +# Change -l value to diff a length other than 300 secs at a time +java -jar rcdiff.jar -e $EXPECTED_LOC -a $ACTUAL_LOC -m 1000 -l 300 diff --git a/hedera-node/test-clients/record-snapshots/TokenUpdatePrecompile.json b/hedera-node/test-clients/record-snapshots/TokenUpdatePrecompile.json new file mode 100644 index 000000000000..5c233194d88e --- /dev/null +++ b/hedera-node/test-clients/record-snapshots/TokenUpdatePrecompile.json @@ -0,0 +1 @@ +{"specSnapshots":{"updateTokenWithInvalidKeyValues":{"placeholderNum":1001,"encodedItems":[{"b64Body":"Cg8KCQiy4J6tBhDLBhICGAISAhgDGPuV9hQiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjloxCiISIFWNdzjCw2BKvPKpJetd3gFDvBNMdtsklnb2vPY8uUr+EICU69wDSgUIgM7aAw==","b64Record":"CiUIFhIDGOoHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjBeGLTpvsDk7c3EgvRIpiGxvVD0U+VgZRsTAIy/SMUXczsSWx+liYsX1kQTU+OQ6BwaDAju4J6tBhCLyOb7AiIPCgkIsuCerQYQywYSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIZCgoKAhgCEP+n1rkHCgsKAxjqBxCAqNa5Bw=="},{"b64Body":"Cg8KCQiz4J6tBhDNBhICGAISAhgDGPuV9hQiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlozCiISIEt8vdPruwDQlHYszhCkb6hpArAxJUieUS1E6TPzioUtEICA6YOx3hZKBQiAztoD","b64Record":"CiUIFhIDGOsHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjAlHcawEGUpJMuwfsQ0AkmcQg5JhUX6PhnpYGJYdZ7w9uL/Dm9iV5Y9u5QLPlrD+N4aDAjv4J6tBhCLme+fASIPCgkIs+CerQYQzQYSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIdCgwKAhgCEP//0YfivC0KDQoDGOsHEICA0ofivC0="},{"b64Body":"Cg8KCQiz4J6tBhDPBhICGAISAhgDGPuV9hQiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjloxCiISIDziEJTMjznYIzTqkmV3irYwTPKr38Cke9vq9EdHiI+MEICU69wDSgUIgM7aAw==","b64Record":"CiUIFhIDGOwHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjAzUL/WBIdiyNJ79BUBh/mXIGDXJHluU3azRP0+9Nae6nJriQAovlpNZYzLB4iapegaDAjv4J6tBhCThZuEAyIPCgkIs+CerQYQzwYSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIZCgoKAhgCEP+n1rkHCgsKAxjsBxCAqNa5Bw=="},{"b64Body":"Cg8KCQi04J6tBhDRBhICGAISAhgDGP7LzSwiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjooBjwESDAjwrvmwBhDIjeqbARptCiISIGAuCV1aCfq2JcZa0UNReVBTEN5VlVJ7bB5qVhAx67umCiM6IQNZoCrtlOqs5weWug61ak87OsYv3/k6tugiLqWacG+rCQoiEiBJc8gbyaX6f0CB4xCggqg0Nt07bvsgsorIfOnfUTZUfCIMSGVsbG8gV29ybGQhKgAyAA==","b64Record":"CiUIFhoDGO0HKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjB5zVKOWyg0Sm0p7NOD2GoDyo6tAEY0xGfrnDfcOJdGSEpJNppLosFmKLANH7+wYcsaDAjw4J6tBhDD0JnGASIPCgkItOCerQYQ0QYSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIA"},{"b64Body":"Cg8KCQi04J6tBhDVBhICGAISAhgDGIydjj4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjpoBiCAKAxjtByKAIDYwODA2MDQwNTI2MDQwNTE4MDYwNDAwMTYwNDA1MjgwNjAwOTgxNTI2MDIwMDE3Zjc0NmY2YjY1NmU0ZTYxNmQ2NTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA4MTUyNTA2MDAyOTA4MTYyMDAwMDRhOTE5MDYyMDAwNTQwNTY1YjUwNjA0MDUxODA2MDQwMDE2MDQwNTI4MDYwMGI4MTUyNjAyMDAxN2Y3NDZmNmI2NTZlNTM3OTZkNjI2ZjZjMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwODE1MjUwNjAwMzkwODE2MjAwMDA5MTkxOTA2MjAwMDU0MDU2NWI1MDYwNDA1MTgwNjA0MDAxNjA0MDUyODA2MDA0ODE1MjYwMjAwMTdmNmQ2NTZkNmYwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgxNTI1MDYwMDQ5MDgxNjIwMDAwZDg5MTkwNjIwMDA1NDA1NjViNTAzNDgwMTU2MjAwMDBlNjU3NjAwMDgwZmQ1YjUwNjAwMTgwNjAwMDgwNjAwNjgxMTExNTYyMDAwMTAyNTc2MjAwMDEwMTYyMDAwNjI3NTY1YjViNjAwNjgxMTExNTYyMDAwMTE3NTc2MjAwMDExNjYyMDAwNjI3NTY1YjViODE1MjYwMjAwMTkwODE1MjYwMjAwMTYwMDAyMDgxOTA1NTUwNjAwMjYwMDE2MDAwNjAwMTYwMDY4MTExMTU2MjAwMDE0NjU3NjIwMDAxNDU2MjAwMDYyNzU2NWI1YjYwMDY4MTExMTU2MjAwMDE1YjU3NjIwMDAxNWE2MjAwMDYyNzU2NWI1YjgxNTI2MDIwMDE5MDgxNTI2MDIwMDE2MDAwMjA4MTkwNTU1MDYwMDQ2MDAxNjAwMDYwMDI2MDA2ODExMTE1NjIwMDAxOGE1NzYyMDAwMTg5NjIwMDA2Mjc1NjViNWI2MDA2ODExMTE1NjIwMDAxOWY1NzYyMDAwMTllNjIwMDA2Mjc1NjViNWI4MTUyNjAyMDAxOTA4MTUyNjAyMDAxNjAwMDIwODE5MDU1NTA2MDA4NjAwMTYwMDA2MDAzNjAwNjgxMTExNTYyMDAwMWNlNTc2MjAwMDFjZDYyMDAwNjI3NTY1YjViNjAwNjgxMTExNTYyMDAwMWUzNTc2MjAwMDFlMjYyMDAwNjI3NTY1YjViODE1MjYwMjAwMTkwODE1MjYwMjAwMTYwMDAyMDgxOTA1NTUwNjAxMDYwMDE2MDAwNjAwNDYwMDY4MTExMTU2MjAwMDIxMjU3NjIwMDAyMTE2MjAwMDYyNzU2NWI1YjYwMDY4MTExMTU2MjAwMDIyNzU3NjIwMDAyMjY2MjAwMDYyNzU2NWI1YjgxNTI2MDIwMDE5MDgxNTI2MDIwMDE2MDAwMjA4MTkwNTU1MDYwMjA2MDAxNjAwMDYwMDU2MDA2ODExMTE1NjIwMDAyNTY1NzYyMDAwMjU1NjIwMDA2Mjc1NjViNWI2MDA2ODExMTE1NjIwMDAyNmI1NzYyMDAwMjZhNjIwMDA2Mjc1NjViNWI4MTUyNjAyMDAxOTA4MTUyNjAyMDAxNjAwMDIwODE5MDU1NTA2MDQwNjAwMTYwMDA2MDA2ODA4MTExMTU2MjAwMDI5OTU3NjIwMDAyOTg2MjAwMDYyNzU2NWI1YjYwMDY4MTExMTU2MjAwMDJhZTU3NjIwMDAyYWQ2MjAwMDYyNzU2NWI1YjgxNTI2MDIwMDE5MDgxNTI2MDIwMDE2MDAwMjA4MTkwNTU1MDYyMDAwNjU2NTY1YjYwMDA4MTUxOTA1MDkxOTA1MDU2NWI3ZjRlNDg3YjcxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA2MDAwNTI2MDQxNjAwNDUyNjAyNDYwMDBmZDViN2Y0ZTQ4N2I3MTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDUyNjAyMjYwMDQ1MjYwMjQ2MDAwZmQ1YjYwMDA2MDAyODIwNDkwNTA2MDAxODIxNjgwNjIwMDAzNDg1NzYwN2Y4MjE2OTE1MDViNjAyMDgyMTA4MTAzNjIwMDAzNWU1NzYyMDAwMzVkNjIwMDAzMDA1NjViNWI1MDkxOTA1MDU2NWI2MDAwODE5MDUwODE2MDAwNTI2MDIwNjAwMDIwOTA1MDkxOTA1MDU2NWI2MDAwNjAyMDYwMWY4MzAxMDQ5MDUwOTE5MDUwNTY1YjYwMDA4MjgyMWI5MDUwOTI5MTUwNTA1NjViNjAwMDYwMDg4MzAyNjIwMDAzYzg3ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmY4MjYyMDAwMzg5NTY1YjYyMDAwM2Q0ODY4MzYyMDAwMzg5NTY1Yjk1NTA4MDE5ODQxNjkzNTA4MDg2MTY4NDE3OTI1MDUwNTA5MzkyNTA1MDUwNTY1YjYwMDA4MTkwNTA5MTkwNTA1NjViNjAwMDgxOTA1MDkxOTA1MDU2NWI2MDAwNjIwMDA0MjE2MjAwMDQxYjYyMDAwNDE1ODQ2MjAwMDNlYzU2NWI2MjAwMDNmNjU2NWI2MjAwMDNlYzU2NWI5MDUwOTE5MDUwNTY1YjYwMDA4MTkwNTA5MTkwNTA1NjViNjIwMDA0M2Q4MzYyMDAwNDAwNTY1YjYyMDAwNDU1NjIwMDA0NGM4MjYyMDAwNDI4NTY1Yjg0ODQ1NDYyMDAwMzk2NTY1YjgyNTU1MDUwNTA1MDU2NWI2MDAwOTA1NjViNjIwMDA0NmM2MjAwMDQ1ZDU2NWI2MjAwMDQ3OTgxODQ4NDYyMDAwNDMyNTY1YjUwNTA1MDU2NWI1YjgxODExMDE1NjIwMDA0YTE1NzYyMDAwNDk1NjAwMDgyNjIwMDA0NjI1NjViNjAwMTgxMDE5MDUwNjIwMDA0N2Y1NjViNTA1MDU2NWI2MDFmODIxMTE1NjIwMDA0ZjA1NzYyMDAwNGJhODE2MjAwMDM2NDU2NWI2MjAwMDRjNTg0NjIwMDAzNzk1NjViODEwMTYwMjA4NTEwMTU2MjAwMDRkNTU3ODE5MDUwNWI2MjAwMDRlZDYyMDAwNGU0ODU2MjAwMDM3OTU2NWI4MzAxODI2MjAwMDQ3ZTU2NWI1MDUwNWI1MDUwNTA1NjViNjAwMDgyODIxYzkwNTA5MjkxNTA1MDU2NWI2MDAwNjIwMDA1MTU2MDAwMTk4NDYwMDgwMjYyMDAwNGY1NTY1YjE5ODA4MzE2OTE1MDUwOTI5MTUwNTA1NjViNjAwMDYyMDAwNTMwODM4MzYyMDAwNTAyNTY1YjkxNTA4MjYwMDIwMjgyMTc5MDUwOTI5MTUwNTA1NjViNjIwMDA1NGI4MjYyMDAwMmM2NTY1YjY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYyMDAwNTY3NTc2MjAwMDU2NjYyMDAwMmQxNTY1YjViNjIwMDA1NzM4MjU0NjIwMDAzMmY1NjViNjIwMDA1ODA4MjgyODU2MjAwMDRhNTU2NWI2MDAwNjAyMDkwNTA2MDFmODMxMTYwMDE4MTE0NjIwMDA1Yjg1NzYwMDA4NDE1NjIwMDA1YTM1NzgyODcwMTUxOTA1MDViNjIwMDA1YWY4NTgyNjIwMDA1MjI1NjViODY1NTUwNjIwMDA2MWY1NjViNjAxZjE5ODQxNjYyMDAwNWM4ODY2MjAwMDM2NDU2NWI2MDAwNWI4MjgxMTAxNTYyMDAwNWYyNTc4NDg5MDE1MTgyNTU2MDAxODIwMTkxNTA2MDIwODUwMTk0NTA2MDIwODEwMTkwNTA2MjAwMDVjYjU2NWI4NjgzMTAxNTYyMDAwNjEyNTc4NDg5MDE1MTYyMDAwNjBlNjAxZjg5MTY4MjYyMDAwNTAyNTY1YjgzNTU1MDViNjAwMTYwMDI4ODAyMDE4ODU1NTA1MDUwNWI1MDUwNTA1MDUwNTA1NjViN2Y0ZTQ4N2I3MTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDUyNjAyMTYwMDQ1MjYwMjQ2MDAwZmQ1YjYxMzk1NjgwNjIwMDA2NjY2MDAwMzk2MDAwZjNmZTYwODA2MDQwNTI2MDA0MzYxMDYxMDBjMjU3NjAwMDM1NjBlMDFjODA2MzdkY2JiYzcyMTE2MTAwN2Y1NzgwNjNlYWM2ZjNmZTExNjEwMDU5NTc4MDYzZWFjNmYzZmUxNDYxMDI1NDU3ODA2M2ViNTQ4ZWZmMTQ2MTAyOTE1NzgwNjNmMDA3Mjk5MDE0NjEwMmFkNTc4MDYzZjE3MzA3NjAxNDYxMDJjOTU3NjEwMGMyNTY1YjgwNjM3ZGNiYmM3MjE0NjEwMWRmNTc4MDYzOWIyM2QzZDkxNDYxMDFmYjU3ODA2M2U2MDhlMThkMTQ2MTAyMzg1NzYxMDBjMjU2NWI4MDYzMTFlMWZjMDcxNDYxMDBjNzU3ODA2MzE1ZGFjYmVhMTQ2MTAxMDQ1NzgwNjMzYWExMmVmNTE0NjEwMTQxNTc4MDYzNDEyOWI3ZGIxNDYxMDE1ZDU3ODA2MzYxOGRjNjVlMTQ2MTAxOWE1NzgwNjM3OGZlNzJhMTE0NjEwMWMzNTc1YjYwMDA4MGZkNWIzNDgwMTU2MTAwZDM1NzYwMDA4MGZkNWI1MDYxMDBlZTYwMDQ4MDM2MDM4MTAxOTA2MTAwZTk5MTkwNjEyMjQzNTY1YjYxMDJlNTU2NWI2MDQwNTE2MTAwZmI5MTkwNjEyMmM2NTY1YjYwNDA1MTgwOTEwMzkwZjM1YjM0ODAxNTYxMDExMDU3NjAwMDgwZmQ1YjUwNjEwMTJiNjAwNDgwMzYwMzgxMDE5MDYxMDEyNjkxOTA2MTIyNDM1NjViNjEwNDAxNTY1YjYwNDA1MTYxMDEzODkxOTA2MTIyYzY1NjViNjA0MDUxODA5MTAzOTBmMzViNjEwMTViNjAwNDgwMzYwMzgxMDE5MDYxMDE1NjkxOTA2MTI0NTM1NjViNjEwNTFmNTY1YjAwNWIzNDgwMTU2MTAxNjk1NzYwMDA4MGZkNWI1MDYxMDE4NDYwMDQ4MDM2MDM4MTAxOTA2MTAxN2Y5MTkwNjEyNTJkNTY1YjYxMDc3MDU2NWI2MDQwNTE2MTAxOTE5MTkwNjEyNjkzNTY1YjYwNDA1MTgwOTEwMzkwZjM=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwOIzROifcodkupftI7bMRuJyO+rHi/xaJKI4/u5BdSOb1SvxW9ft3/miEy9yhdtjJGgwI8OCerQYQ85G+qwMiDwoJCLTgnq0GENUGEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQi14J6tBhDbBhICGAISAhgDGPjR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxjtByKAIDViMzQ4MDE1NjEwMWE2NTc2MDAwODBmZDViNTA2MTAxYzE2MDA0ODAzNjAzODEwMTkwNjEwMWJjOTE5MDYxMjZiNTU2NWI2MTA3YTc1NjViMDA1YjYxMDFkZDYwMDQ4MDM2MDM4MTAxOTA2MTAxZDg5MTkwNjEyNzExNTY1YjYxMDhjZTU2NWIwMDViNjEwMWY5NjAwNDgwMzYwMzgxMDE5MDYxMDFmNDkxOTA2MTI3YjA1NjViNjEwYTY1NTY1YjAwNWIzNDgwMTU2MTAyMDc1NzYwMDA4MGZkNWI1MDYxMDIyMjYwMDQ4MDM2MDM4MTAxOTA2MTAyMWQ5MTkwNjEyMjQzNTY1YjYxMGI4NTU2NWI2MDQwNTE2MTAyMmY5MTkwNjEyMmM2NTY1YjYwNDA1MTgwOTEwMzkwZjM1YjYxMDI1MjYwMDQ4MDM2MDM4MTAxOTA2MTAyNGQ5MTkwNjEyOGE0NTY1YjYxMGNhMzU2NWIwMDViMzQ4MDE1NjEwMjYwNTc2MDAwODBmZDViNTA2MTAyN2I2MDA0ODAzNjAzODEwMTkwNjEwMjc2OTE5MDYxMjI0MzU2NWI2MTBlODA1NjViNjA0MDUxNjEwMjg4OTE5MDYxMjJjNjU2NWI2MDQwNTE4MDkxMDM5MGYzNWI2MTAyYWI2MDA0ODAzNjAzODEwMTkwNjEwMmE2OTE5MDYxMmEwZjU2NWI2MTBmOWM1NjViMDA1YjYxMDJjNzYwMDQ4MDM2MDM4MTAxOTA2MTAyYzI5MTkwNjEyYWMyNTY1YjYxMTMzNDU2NWIwMDViNjEwMmUzNjAwNDgwMzYwMzgxMDE5MDYxMDJkZTkxOTA2MTJiMDI1NjViNjExNTg3NTY1YjAwNWI2MDAwODA2MDAwNjEwMTY3NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY2MzliMjNkM2Q5NjBlMDFiODg4ODg4ODg2MDQwNTE2MDI0MDE2MTAzMjI5NDkzOTI5MTkwNjEyYmJmNTY1YjYwNDA1MTYwMjA4MTgzMDMwMzgxNTI5MDYwNDA1MjkwN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE5MTY2MDIwODIwMTgwNTE3YmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmODM4MTgzMTYxNzgzNTI1MDUwNTA1MDYwNDA1MTYxMDM4YzkxOTA2MTJjNDA1NjViNjAwMDYwNDA1MTgwODMwMzgxODU1YWY0OTE1MDUwM2Q4MDYwMDA4MTE0NjEwM2M3NTc2MDQwNTE5MTUwNjAxZjE5NjAzZjNkMDExNjgyMDE2MDQwNTIzZDgyNTIzZDYwMDA2MDIwODQwMTNlNjEwM2NjNTY1YjYwNjA5MTUwNWI1MDkxNTA5MTUwODE2MTAzZGQ1NzYwMTU2MTAzZjI1NjViODA4MDYwMjAwMTkwNTE4MTAxOTA2MTAzZjE5MTkwNjEyYzkwNTY1YjViNjAwMzBiOTI1MDUwNTA5NDkzNTA1MDUwNTA1NjViNjAwMDgwNjAwMDYxMDE2NzczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2NjMxNWRhY2JlYTYwZTAxYjg4ODg4ODg4NjA0MDUxNjAyNDAxNjEwNDNlOTQ5MzkyOTE5MDYxMmJiZjU2NWI2MDQwNTE2MDIwODE4MzAzMDM4MTUyOTA2MDQwNTI5MDdiZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxOTE2NjAyMDgyMDE4MDUxN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjgzODE4MzE2MTc4MzUyNTA1MDUwNTA2MDQwNTE2MTA0YTg5MTkwNjEyYzQwNTY1YjYwMDA2MDQwNTE4MDgzMDM4MTYwMDA4NjVhZjE5MTUwNTAzZDgwNjAwMDgxMTQ2MTA0ZTU1NzYwNDA1MTkxNTA2MDFmMTk2MDNmM2QwMTE2ODIwMTYwNDA1MjNkODI1MjNkNjAwMDYwMjA4NDAxM2U2MTA0ZWE1NjViNjA2MDkxNTA1YjUwOTE1MDkxNTA4MTYxMDRmYjU3NjAxNTYxMDUxMDU2NWI4MDgwNjAyMDAxOTA1MTgxMDE5MDYxMDUwZjkxOTA2MTJjOTA1NjViNWI2MDAzMGI5MjUwNTA1MDk0OTM1MDUwNTA1MDU2NWI2MDAwNjAwNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMDUzYzU3NjEwNTNiNjEyMmZjNTY1YjViNjA0MDUxOTA4MDgyNTI4MDYwMjAwMjYwMjAwMTgyMDE2MDQwNTI4MDE1NjEwNTc1NTc4MTYwMjAwMTViNjEwNTYyNjEyMDcyNTY1YjgxNTI2MDIwMDE5MDYwMDE5MDAzOTA4MTYxMDU1YTU3OTA1MDViNTA5MDUwNjEwNTg3NjAwMDYwMDE2MDAyODk2MTE2Yzg1NjViODE2MDAwODE1MTgxMTA2MTA1OWI1NzYxMDU5YTYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTA1YjQ2MDAyNjAwMzgwODg2MTE2Yzg1NjViODE2MDAxODE1MTgxMTA2MTA1Yzg1NzYxMDVjNzYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTA1ZTA2MDA0NjAwMTg2NjExNzAxNTY1YjgxNjAwMjgxNTE4MTEwNjEwNWY0NTc2MTA1ZjM2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwNjEwNjBjNjAwNjYwMDE4NjYxMTcwMTU2NWI4MTYwMDM4MTUxODExMDYxMDYyMDU3NjEwNjFmNjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMDYzODYwMDU2MDA0ODY2MTE3MDE1NjViODE2MDA0ODE1MTgxMTA2MTA2NGM1NzYxMDY0YjYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTA2NWY2MTIwOTI1NjViNjAwMDgxNjAwMDAxOTA2MDA3MGI5MDgxNjAwNzBiODE1MjUwNTA4MzgxNjAyMDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwODI4MTYwNDAwMTkwNjAwNzBiOTA4MTYwMDcwYjgxNTI1MDUwNjEwNmM0NjEyMGNmNTY1Yjg4ODE2MDQwMDE5MDczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2OTA4MTczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2ODE1MjUwNTA4MjgxNjBlMDAxODE5MDUyNTA4MTgxNjEwMTAwMDE4MTkwNTI1MDYwMDA2MTA3MWI4YjgzNjExNzM4NTY1YjkwNTA2MDE2NjAwMzBiODExNDYxMDc2MzU3NjA0MDUxN2YwOGMzNzlhMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwODE1MjYwMDQwMTYxMDc1YTkwNjEyZDQ5NTY1YjYwNDA1MTgwOTEwMzkwZmQ1YjUwNTA1MDUwNTA1MDUwNTA1MDUwNTA1NjViNjEwNzc4NjEyMTNlNTY1YjYwMDA4MDYxMDc4NTg1ODU2MTE4NTA1NjViOTE1MDYwMDcwYjkxNTA2MDE2NjAwMzBiODIxNDYxMDc5YzU3NjAwMDgwZmQ1YjgwOTI1MDUwNTA5MjkxNTA1MDU2NWI2MDAwODA2MTAxNjc3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjYzNjE4ZGM2NWU2MGUwMWI4NTg1NjA0MDUxNjAyNDAxNjEwN2RlOTI5MTkwNjEyZGIzNTY1YjYwNDA1MTYwMjA4MTgzMDMwMzgxNTI5MDYwNDA1MjkwN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE5MTY2MDIwODIwMTgwNTE3YmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmODM4MTgzMTYxNzgzNTI1MDUwNTA1MDYwNDA1MTYxMDg0ODkxOTA2MTJjNDA1NjViNjAwMDYwNDA1MTgwODMwMzgxNjAwMDg2NWFmMTkxNTA1MDNkODA2MDAwODExNDYxMDg4NTU3NjA0MDUxOTE1MDYwMWYxOTYwM2YzZDAxMTY4MjAxNjA0MDUyM2Q4MjUyM2Q2MDAwNjAyMDg0MDEzZTYxMDg4YTU2NWI2MDYwOTE1MDViNTA5MTUwOTE1MDdmNGFmNDc4MGUwNmZlOGNiOWRmNjRiMDc5NGZhNmYwMTM5OWFmOTc5MTc1YmI5ODhlMzVlMGU1N2U1OTQ1NjdiYzgyODI2MDQwNTE2MTA4YzA5MjkxOTA2MTJkZjI1NjViNjA0MDUxODA5MTAzOTBhMTUwNTA1MDUwNTY1YjYwMDA2MDA1NjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEwOGViNTc2MTA4ZWE2MTIyZmM1NjViNWI2MDQwNTE5MDgwODI1MjgwNjAyMDAyNjAyMDAxODIwMTYwNDA1MjgwMTU2MTA5MjQ1NzgxNjAyMDAxNWI2MTA5MTE2MTIwNzI1NjViODE1MjYwMjAwMTkwNjAwMTkwMDM5MDgxNjEwOTA5NTc5MDUwNWI1MDkwNTA2MTA5MzY2MDAwNjAwMTYwMDI4NzYxMTZjODU2NWI4MTYwMDA4MTUxODExMDYxMDk0YTU3NjEwOTQ5NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMDk2MzYwMDI2MDAzODA4NjYxMTZjODU2NWI4MTYwMDE4MTUxODExMDYxMDk3NzU3NjEwOTc2NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMDk4ZjYwMDQ2MDAxODQ2MTE3MDE1NjViODE2MDAyODE1MTgxMTA2MTA5YTM=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwcd3PGCA5rUapGTu1nfjeu43B4liRHsEJw4VmAQW31CGr6q7onS9CX5OPwoNLhDpCGgwI8eCerQYQ26GLzwEiDwoJCLXgnq0GENsGEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQi14J6tBhDhBhICGAISAhgDGPjR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxjtByKAIDU3NjEwOWEyNjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMDliYjYwMDY2MDAxODQ2MTE3MDE1NjViODE2MDAzODE1MTgxMTA2MTA5Y2Y1NzYxMDljZTYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTA5ZTc2MDA1NjAwNDg0NjExNzAxNTY1YjgxNjAwNDgxNTE4MTEwNjEwOWZiNTc2MTA5ZmE2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwNjAwMDYxMGExMjg2ODM2MTE5ODQ1NjViNjAwNzBiOTA1MDYwMTY2MDAzMGI4MTE0NjEwYTVkNTc2MDQwNTE3ZjA4YzM3OWEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA4MTUyNjAwNDAxNjEwYTU0OTA2MTJlNmU1NjViNjA0MDUxODA5MTAzOTBmZDViNTA1MDUwNTA1MDUwNTY1YjYwMDA2MDAxNjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEwYTgyNTc2MTBhODE2MTIyZmM1NjViNWI2MDQwNTE5MDgwODI1MjgwNjAyMDAyNjAyMDAxODIwMTYwNDA1MjgwMTU2MTBhYmI1NzgxNjAyMDAxNWI2MTBhYTg2MTIwNzI1NjViODE1MjYwMjAwMTkwNjAwMTkwMDM5MDgxNjEwYWEwNTc5MDUwNWI1MDkwNTA2MTBhYzY2MTIwNzI1NjViNjAwNDgxNjAwMDAxODE4MTUyNTA1MDYxMGFkOTYxMjEzZTU2NWIzMDgxNjAyMDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwNjAwMTgxNjAwMDAxOTAxNTE1OTA4MTE1MTU4MTUyNTA1MDgwODI2MDIwMDE4MTkwNTI1MDgxODM2MDAwODE1MTgxMTA2MTBiNDA1NzYxMGIzZjYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MDAwNjEwYjViMzA2MDAwODg4ODg4NjExYTljNTY1YjkwNTA2MDAwNjEwYjY5ODg4MzYxMTczODU2NWI5MDUwNjAxNjYwMDMwYjgxMTQ2MTBiN2I1NzYwMDA4MGZkNWI1MDUwNTA1MDUwNTA1MDUwNTY1YjYwMDA4MDYwMDA2MTAxNjc3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjYzOWIyM2QzZDk2MGUwMWI4ODg4ODg4ODYwNDA1MTYwMjQwMTYxMGJjMjk0OTM5MjkxOTA2MTJiYmY1NjViNjA0MDUxNjAyMDgxODMwMzAzODE1MjkwNjA0MDUyOTA3YmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTkxNjYwMjA4MjAxODA1MTdiZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmY4MzgxODMxNjE3ODM1MjUwNTA1MDUwNjA0MDUxNjEwYzJjOTE5MDYxMmM0MDU2NWI2MDAwNjA0MDUxODA4MzAzODE2MDAwODY1YWYxOTE1MDUwM2Q4MDYwMDA4MTE0NjEwYzY5NTc2MDQwNTE5MTUwNjAxZjE5NjAzZjNkMDExNjgyMDE2MDQwNTIzZDgyNTIzZDYwMDA2MDIwODQwMTNlNjEwYzZlNTY1YjYwNjA5MTUwNWI1MDkxNTA5MTUwODE2MTBjN2Y1NzYwMTU2MTBjOTQ1NjViODA4MDYwMjAwMTkwNTE4MTAxOTA2MTBjOTM5MTkwNjEyYzkwNTY1YjViNjAwMzBiOTI1MDUwNTA5NDkzNTA1MDUwNTA1NjViNjAwMDYwMDU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTBjYzA1NzYxMGNiZjYxMjJmYzU2NWI1YjYwNDA1MTkwODA4MjUyODA2MDIwMDI2MDIwMDE4MjAxNjA0MDUyODAxNTYxMGNmOTU3ODE2MDIwMDE1YjYxMGNlNjYxMjA3MjU2NWI4MTUyNjAyMDAxOTA2MDAxOTAwMzkwODE2MTBjZGU1NzkwNTA1YjUwOTA1MDYxMGQwYjYwMDA2MDAxNjAwMjhjNjExNmM4NTY1YjgxNjAwMDgxNTE4MTEwNjEwZDFmNTc2MTBkMWU2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwNjEwZDM4NjAwMjYwMDM4MDhiNjExNmM4NTY1YjgxNjAwMTgxNTE4MTEwNjEwZDRjNTc2MTBkNGI2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwNjEwZDY0NjAwNDYwMDE4OTYxMTcwMTU2NWI4MTYwMDI4MTUxODExMDYxMGQ3ODU3NjEwZDc3NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMGQ5MDYwMDY2MDAxODk2MTE3MDE1NjViODE2MDAzODE1MTgxMTA2MTBkYTQ1NzYxMGRhMzYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTBkYmM2MDA1NjAwNDg5NjExNzAxNTY1YjgxNjAwNDgxNTE4MTEwNjEwZGQwNTc2MTBkY2Y2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwODM2MDAyOTA4MTYxMGRlYTkxOTA2MTMwYTU1NjViNTA4MjYwMDM5MDgxNjEwZGZhOTE5MDYxMzBhNTU2NWI1MDgxNjAwNDkwODE2MTBlMGE5MTkwNjEzMGE1NTY1YjUwNjAwMDYxMGUxYjhiNjAwMDg5ODk4NjYxMWE5YzU2NWI5MDUwNjAwMDYxMGUyOThkODM2MTE3Mzg1NjViOTA1MDYwMTY2MDAzMGI4MTE0NjEwZTcxNTc2MDQwNTE3ZjA4YzM3OWEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA4MTUyNjAwNDAxNjEwZTY4OTA2MTJkNDk1NjViNjA0MDUxODA5MTAzOTBmZDViNTA1MDUwNTA1MDUwNTA1MDUwNTA1MDUwNTA1NjViNjAwMDgwNjAwMDYxMDE2NzczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2NjMxNWRhY2JlYTYwZTAxYjg4ODg4ODg4NjA0MDUxNjAyNDAxNjEwZWJkOTQ5MzkyOTE5MDYxMmJiZjU2NWI2MDQwNTE2MDIwODE4MzAzMDM4MTUyOTA2MDQwNTI5MDdiZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxOTE2NjAyMDgyMDE4MDUxN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjgzODE4MzE2MTc4MzUyNTA1MDUwNTA2MDQwNTE2MTBmMjc5MTkwNjEyYzQwNTY1YjYwMDA2MDQwNTE4MDgzMDM4MTg1NWFmNDkxNTA1MDNkODA2MDAwODExNDYxMGY2MjU3NjA0MDUxOTE1MDYwMWYxOTYwM2YzZDAxMTY4MjAxNjA0MDUyM2Q4MjUyM2Q2MDAwNjAyMDg0MDEzZTYxMGY2NzU2NWI2MDYwOTE1MDViNTA5MTUwOTE1MDgxNjEwZjc4NTc2MDE1NjEwZjhkNTY1YjgwODA2MDIwMDE5MDUxODEwMTkwNjEwZjhjOTE5MDYxMmM5MDU2NWI1YjYwMDMwYjkyNTA1MDUwOTQ5MzUwNTA1MDUwNTY1YjYwMDA2MDA1NjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEwZmI5NTc2MTBmYjg2MTIyZmM1NjViNWI2MDQwNTE5MDgwODI1MjgwNjAyMDAyNjAyMDAxODIwMTYwNDA1MjgwMTU2MTBmZjI1NzgxNjAyMDAxNWI2MTBmZGY2MTIwNzI1NjViODE1MjYwMjAwMTkwNjAwMTkwMDM5MDgxNjEwZmQ3NTc5MDUwNWI1MDkwNTA2MTEwMDQ2MDAwNjAwMTYwMDI4NzYxMTZjODU2NWI4MTYwMDA4MTUxODExMDYxMTAxODU3NjExMDE3NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMTAzMTYwMDI2MDAzODA4NjYxMTZjODU2NWI4MTYwMDE4MTUxODExMDYxMTA0NTU3NjExMDQ0NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMTA1ZDYwMDQ2MDAxODQ2MTE3MDE1NjViODE2MDAyODE1MTgxMTA2MTEwNzE1NzYxMTA3MDYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTEwODk2MDA2NjAwMTg0NjExNzAxNTY1YjgxNjAwMzgxNTE4MTEwNjExMDlkNTc2MTEwOWM2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwNjExMGI1NjAwNTYwMDQ4NDYxMTcwMTU2NWI4MTYwMDQ4MTUxODExMDYxMTBjOTU3NjExMGM4NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMTBkYzYxMjBjZjU2NWI4NTgxNjA0MDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwNjAwMjgwNTQ2MTExMjE5MDYxMmVjODU2NWI4MDYwMWYwMTYwMjA4MDkxMDQwMjYwMjAwMTYwNDA1MTkwODEwMTYwNDA1MjgwOTI5MTkwODE4MTUyNjAyMDAxODI4MDU0NjExMTRkOTA2MTJlYzg1NjViODAxNTYxMTE5YTU3ODA2MDFmMTA2MTExNmY1NzYxMDEwMDgwODM1NDA0MDI4MzUyOTE2MDIwMDE5MTYxMTE5YTU2NWI4MjAxOTE5MDYwMDA1MjYwMjA2MDAwMjA5MDViODE1NDgxNTI5MDYwMDEwMTkwNjAyMDAxODA4MzExNjExMTdkNTc4MjkwMDM2MDFmMTY4MjAxOTE=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIw9g2F8fqVYnOrkhsYLhC5Bd/7FflRmTQNCpUtqSXL/Ht+HuQ/Cuf4rC55Eq8/ohwzGgwI8eCerQYQ26u31QMiDwoJCLXgnq0GEOEGEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQi24J6tBhDnBhICGAISAhgDGPfR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxjtByKAIDViNTA1MDUwNTA1MDgxNjAwMDAxODE5MDUyNTA2MDAzODA1NDYxMTFiNDkwNjEyZWM4NTY1YjgwNjAxZjAxNjAyMDgwOTEwNDAyNjAyMDAxNjA0MDUxOTA4MTAxNjA0MDUyODA5MjkxOTA4MTgxNTI2MDIwMDE4MjgwNTQ2MTExZTA5MDYxMmVjODU2NWI4MDE1NjExMjJkNTc4MDYwMWYxMDYxMTIwMjU3NjEwMTAwODA4MzU0MDQwMjgzNTI5MTYwMjAwMTkxNjExMjJkNTY1YjgyMDE5MTkwNjAwMDUyNjAyMDYwMDAyMDkwNWI4MTU0ODE1MjkwNjAwMTAxOTA2MDIwMDE4MDgzMTE2MTEyMTA1NzgyOTAwMzYwMWYxNjgyMDE5MTViNTA1MDUwNTA1MDgxNjAyMDAxODE5MDUyNTA4MTgxNjBlMDAxODE5MDUyNTA2MDA0ODA1NDYxMTI1MDkwNjEyZWM4NTY1YjgwNjAxZjAxNjAyMDgwOTEwNDAyNjAyMDAxNjA0MDUxOTA4MTAxNjA0MDUyODA5MjkxOTA4MTgxNTI2MDIwMDE4MjgwNTQ2MTEyN2M5MDYxMmVjODU2NWI4MDE1NjExMmM5NTc4MDYwMWYxMDYxMTI5ZTU3NjEwMTAwODA4MzU0MDQwMjgzNTI5MTYwMjAwMTkxNjExMmM5NTY1YjgyMDE5MTkwNjAwMDUyNjAyMDYwMDAyMDkwNWI4MTU0ODE1MjkwNjAwMTAxOTA2MDIwMDE4MDgzMTE2MTEyYWM1NzgyOTAwMzYwMWYxNjgyMDE5MTViNTA1MDUwNTA1MDgxNjA2MDAxODE5MDUyNTA2MDAwNjExMmUyODg4MzYxMTczODU2NWI5MDUwNjAxNjYwMDMwYjgxMTQ2MTEzMmE1NzYwNDA1MTdmMDhjMzc5YTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgxNTI2MDA0MDE2MTEzMjE5MDYxMzFjMzU2NWI2MDQwNTE4MDkxMDM5MGZkNWI1MDUwNTA1MDUwNTA1MDUwNTY1YjYxMTMzYzYxMjBjZjU2NWI2MDAyODA1NDYxMTM0OTkwNjEyZWM4NTY1YjgwNjAxZjAxNjAyMDgwOTEwNDAyNjAyMDAxNjA0MDUxOTA4MTAxNjA0MDUyODA5MjkxOTA4MTgxNTI2MDIwMDE4MjgwNTQ2MTEzNzU5MDYxMmVjODU2NWI4MDE1NjExM2MyNTc4MDYwMWYxMDYxMTM5NzU3NjEwMTAwODA4MzU0MDQwMjgzNTI5MTYwMjAwMTkxNjExM2MyNTY1YjgyMDE5MTkwNjAwMDUyNjAyMDYwMDAyMDkwNWI4MTU0ODE1MjkwNjAwMTAxOTA2MDIwMDE4MDgzMTE2MTEzYTU1NzgyOTAwMzYwMWYxNjgyMDE5MTViNTA1MDUwNTA1MDgxNjAwMDAxODE5MDUyNTA2MDAzODA1NDYxMTNkYzkwNjEyZWM4NTY1YjgwNjAxZjAxNjAyMDgwOTEwNDAyNjAyMDAxNjA0MDUxOTA4MTAxNjA0MDUyODA5MjkxOTA4MTgxNTI2MDIwMDE4MjgwNTQ2MTE0MDg5MDYxMmVjODU2NWI4MDE1NjExNDU1NTc4MDYwMWYxMDYxMTQyYTU3NjEwMTAwODA4MzU0MDQwMjgzNTI5MTYwMjAwMTkxNjExNDU1NTY1YjgyMDE5MTkwNjAwMDUyNjAyMDYwMDAyMDkwNWI4MTU0ODE1MjkwNjAwMTAxOTA2MDIwMDE4MDgzMTE2MTE0Mzg1NzgyOTAwMzYwMWYxNjgyMDE5MTViNTA1MDUwNTA1MDgxNjAyMDAxODE5MDUyNTA4MTgxNjA0MDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwNjAwNDgwNTQ2MTE0YTc5MDYxMmVjODU2NWI4MDYwMWYwMTYwMjA4MDkxMDQwMjYwMjAwMTYwNDA1MTkwODEwMTYwNDA1MjgwOTI5MTkwODE4MTUyNjAyMDAxODI4MDU0NjExNGQzOTA2MTJlYzg1NjViODAxNTYxMTUyMDU3ODA2MDFmMTA2MTE0ZjU1NzYxMDEwMDgwODM1NDA0MDI4MzUyOTE2MDIwMDE5MTYxMTUyMDU2NWI4MjAxOTE5MDYwMDA1MjYwMjA2MDAwMjA5MDViODE1NDgxNTI5MDYwMDEwMTkwNjAyMDAxODA4MzExNjExNTAzNTc4MjkwMDM2MDFmMTY4MjAxOTE1YjUwNTA1MDUwNTA4MTYwNjAwMTgxOTA1MjUwNjAwMDYxMTUzOTg0ODM2MTE3Mzg1NjViOTA1MDYwMTY2MDAzMGI4MTE0NjExNTgxNTc2MDQwNTE3ZjA4YzM3OWEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA4MTUyNjAwNDAxNjExNTc4OTA2MTMyNTU1NjViNjA0MDUxODA5MTAzOTBmZDViNTA1MDUwNTA1NjViNjExNThmNjEyMGNmNTY1YjgyODE2MDAwMDE4MTkwNTI1MDgxODE2MDIwMDE4MTkwNTI1MDgzODE2MDQwMDE5MDczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2OTA4MTczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2ODE1MjUwNTA2MDA0ODA1NDYxMTVlNjkwNjEyZWM4NTY1YjgwNjAxZjAxNjAyMDgwOTEwNDAyNjAyMDAxNjA0MDUxOTA4MTAxNjA0MDUyODA5MjkxOTA4MTgxNTI2MDIwMDE4MjgwNTQ2MTE2MTI5MDYxMmVjODU2NWI4MDE1NjExNjVmNTc4MDYwMWYxMDYxMTYzNDU3NjEwMTAwODA4MzU0MDQwMjgzNTI5MTYwMjAwMTkxNjExNjVmNTY1YjgyMDE5MTkwNjAwMDUyNjAyMDYwMDAyMDkwNWI4MTU0ODE1MjkwNjAwMTAxOTA2MDIwMDE4MDgzMTE2MTE2NDI1NzgyOTAwMzYwMWYxNjgyMDE5MTViNTA1MDUwNTA1MDgxNjA2MDAxODE5MDUyNTA2MDAwNjExNjc4ODY4MzYxMTczODU2NWI5MDUwNjAxNjYwMDMwYjgxMTQ2MTE2YzA1NzYwNDA1MTdmMDhjMzc5YTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgxNTI2MDA0MDE2MTE2Yjc5MDYxMzJlNzU2NWI2MDQwNTE4MDkxMDM5MGZkNWI1MDUwNTA1MDUwNTA1NjViNjExNmQwNjEyMDcyNTY1YjYwNDA1MTgwNjA0MDAxNjA0MDUyODA2MTE2ZTU4Nzg3NjExZDE2NTY1YjgxNTI2MDIwMDE2MTE2ZjQ4NTg1NjExZDZjNTY1YjgxNTI1MDkwNTA5NDkzNTA1MDUwNTA1NjViNjExNzA5NjEyMDcyNTY1YjYwNDA1MTgwNjA0MDAxNjA0MDUyODA2MTE3MWQ4NjYxMWY0MTU2NWI4MTUyNjAyMDAxNjExNzJjODU4NTYxMWY4MjU2NWI4MTUyNTA5MDUwOTM5MjUwNTA1MDU2NWI2MDAwODA2MDAwNjEwMTY3NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY2MzdkMzA1Y2ZhNjBlMDFiODY4NjYwNDA1MTYwMjQwMTYxMTc3MTkyOTE5MDYxMzYwNzU2NWI2MDQwNTE2MDIwODE4MzAzMDM4MTUyOTA2MDQwNTI5MDdiZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxOTE2NjAyMDgyMDE4MDUxN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjgzODE4MzE2MTc4MzUyNTA1MDUwNTA2MDQwNTE2MTE3ZGI5MTkwNjEyYzQwNTY1YjYwMDA2MDQwNTE4MDgzMDM4MTYwMDA4NjVhZjE5MTUwNTAzZDgwNjAwMDgxMTQ2MTE4MTg1NzYwNDA1MTkxNTA2MDFmMTk2MDNmM2QwMTE2ODIwMTYwNDA1MjNkODI1MjNkNjAwMDYwMjA4NDAxM2U2MTE4MWQ1NjViNjA2MDkxNTA1YjUwOTE1MDkxNTA4MTYxMTgyZTU3NjAxNTYxMTg0MzU2NWI4MDgwNjAyMDAxOTA1MTgxMDE5MDYxMTg0MjkxOTA2MTJjOTA1NjViNWI2MDAzMGI5MjUwNTA1MDkyOTE1MDUwNTY1YjYwMDA2MTE4NWE2MTIxM2U1NjViNjAwMDgwNjEwMTY3NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY2MzNjNGRkMzJlNjBlMDFiODc4NzYwNDA1MTYwMjQwMTYxMTg5MTkyOTE5MDYxMzYzNzU2NWI2MDQwNTE2MDIwODE4MzAzMDM4MTUyOTA2MDQwNTI5MDdiZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxOTE2NjAyMDgyMDE4MDUxN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjgzODE4MzE2MTc4MzUyNTA1MDUwNTA2MDQwNTE2MTE4ZmI5MTkwNjEyYzQwNTY1YjYwMDA2MDQwNTE4MDgzMDM4MTYwMDA4NjVhZjE5MTUwNTAzZDgwNjAwMDgxMTQ2MTE5Mzg1NzYwNDA1MTkxNTA2MDFmMTk2MDNmM2QwMTE2ODIwMTYwNDA1MjNkODI1MjNkNjAwMDYwMjA4NDAxM2U2MTE5M2Q1NjViNjA2MDkxNTA1YjUwOTE1MDkxNTA2MTE5NGE2MTIxM2U1NjViODI2MTE5NTc1NzYwMTU4MTYxMTk2YzU2NWI4MTgwNjAyMDAxOTA1MTgxMDE5MDYxMTk2YjkxOTA2MTM3ZGY1NjViNWI4MTYwMDMwYjkxNTA4MDk1NTA4MTk2NTA1MDUwNTA1MDUwOTI1MDkyOTA1MDU2NWI2MDAwODA2MDAwNjEwMTY3NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmY=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIweCdBa65y91cBvs4Yak8A9V4wpTntu5p1OO+hnJY12oTpsAeZblwFOxDsbb2lJLq4GgwI8uCerQYQ29z03QEiDwoJCLbgnq0GEOcGEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQi24J6tBhDtBhICGAISAhgDGPfR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxjtByKAIGZmZmZmZmZmZmZmZmZmZmYxNjYzNmZjM2NiYWY2MGUwMWI4Njg2NjA0MDUxNjAyNDAxNjExOWJkOTI5MTkwNjEzOGMxNTY1YjYwNDA1MTYwMjA4MTgzMDMwMzgxNTI5MDYwNDA1MjkwN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE5MTY2MDIwODIwMTgwNTE3YmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmODM4MTgzMTYxNzgzNTI1MDUwNTA1MDYwNDA1MTYxMWEyNzkxOTA2MTJjNDA1NjViNjAwMDYwNDA1MTgwODMwMzgxNjAwMDg2NWFmMTkxNTA1MDNkODA2MDAwODExNDYxMWE2NDU3NjA0MDUxOTE1MDYwMWYxOTYwM2YzZDAxMTY4MjAxNjA0MDUyM2Q4MjUyM2Q2MDAwNjAyMDg0MDEzZTYxMWE2OTU2NWI2MDYwOTE1MDViNTA5MTUwOTE1MDgxNjExYTdhNTc2MDE1NjExYThmNTY1YjgwODA2MDIwMDE5MDUxODEwMTkwNjExYThlOTE5MDYxMmM5MDU2NWI1YjYwMDMwYjkyNTA1MDUwOTI5MTUwNTA1NjViNjExYWE0NjEyMGNmNTY1YjYxMWFhYzYxMjA5MjU2NWI4NTgxNjAwMDAxOTA2MDA3MGI5MDgxNjAwNzBiODE1MjUwNTA4NDgxNjAyMDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwODM4MTYwNDAwMTkwNjAwNzBiOTA4MTYwMDcwYjgxNTI1MDUwNjAwMjgwNTQ2MTFiMTU5MDYxMmVjODU2NWI4MDYwMWYwMTYwMjA4MDkxMDQwMjYwMjAwMTYwNDA1MTkwODEwMTYwNDA1MjgwOTI5MTkwODE4MTUyNjAyMDAxODI4MDU0NjExYjQxOTA2MTJlYzg1NjViODAxNTYxMWI4ZTU3ODA2MDFmMTA2MTFiNjM1NzYxMDEwMDgwODM1NDA0MDI4MzUyOTE2MDIwMDE5MTYxMWI4ZTU2NWI4MjAxOTE5MDYwMDA1MjYwMjA2MDAwMjA5MDViODE1NDgxNTI5MDYwMDEwMTkwNjAyMDAxODA4MzExNjExYjcxNTc4MjkwMDM2MDFmMTY4MjAxOTE1YjUwNTA1MDUwNTA4MjYwMDAwMTgxOTA1MjUwNjAwMzgwNTQ2MTFiYTg5MDYxMmVjODU2NWI4MDYwMWYwMTYwMjA4MDkxMDQwMjYwMjAwMTYwNDA1MTkwODEwMTYwNDA1MjgwOTI5MTkwODE4MTUyNjAyMDAxODI4MDU0NjExYmQ0OTA2MTJlYzg1NjViODAxNTYxMWMyMTU3ODA2MDFmMTA2MTFiZjY1NzYxMDEwMDgwODM1NDA0MDI4MzUyOTE2MDIwMDE5MTYxMWMyMTU2NWI4MjAxOTE5MDYwMDA1MjYwMjA2MDAwMjA5MDViODE1NDgxNTI5MDYwMDEwMTkwNjAyMDAxODA4MzExNjExYzA0NTc4MjkwMDM2MDFmMTY4MjAxOTE1YjUwNTA1MDUwNTA4MjYwMjAwMTgxOTA1MjUwODY4MjYwNDAwMTkwNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY5MDgxNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTUyNTA1MDgyODI2MGUwMDE4MTkwNTI1MDgwODI2MTAxMDAwMTgxOTA1MjUwNjAwNDgwNTQ2MTFjODY5MDYxMmVjODU2NWI4MDYwMWYwMTYwMjA4MDkxMDQwMjYwMjAwMTYwNDA1MTkwODEwMTYwNDA1MjgwOTI5MTkwODE4MTUyNjAyMDAxODI4MDU0NjExY2IyOTA2MTJlYzg1NjViODAxNTYxMWNmZjU3ODA2MDFmMTA2MTFjZDQ1NzYxMDEwMDgwODM1NDA0MDI4MzUyOTE2MDIwMDE5MTYxMWNmZjU2NWI4MjAxOTE5MDYwMDA1MjYwMjA2MDAwMjA5MDViODE1NDgxNTI5MDYwMDEwMTkwNjAyMDAxODA4MzExNjExY2UyNTc4MjkwMDM2MDFmMTY4MjAxOTE1YjUwNTA1MDUwNTA4MjYwNjAwMTgxOTA1MjUwNTA5NTk0NTA1MDUwNTA1MDU2NWI2MDAwNjExZDNkODM2MDA2ODExMTE1NjExZDJlNTc2MTFkMmQ2MTM4ZjE1NjViNWI4MjYxMjA1ZTkwOTE5MDYzZmZmZmZmZmYxNjU2NWI5MDUwNjExZDY0ODI2MDA2ODExMTE1NjExZDU1NTc2MTFkNTQ2MTM4ZjE1NjViNWI4MjYxMjA1ZTkwOTE5MDYzZmZmZmZmZmYxNjU2NWI5MDUwOTI5MTUwNTA1NjViNjExZDc0NjEyMTNlNTY1YjYwMDA2MDA0ODExMTE1NjExZDg4NTc2MTFkODc2MTM4ZjE1NjViNWI4MzYwMDQ4MTExMTU2MTFkOWI1NzYxMWQ5YTYxMzhmMTU2NWI1YjAzNjExZGI2NTc2MDAxODE2MDAwMDE5MDE1MTU5MDgxMTUxNTgxNTI1MDUwNjExZjNiNTY1YjYwMDE2MDA0ODExMTE1NjExZGNhNTc2MTFkYzk2MTM4ZjE1NjViNWI4MzYwMDQ4MTExMTU2MTFkZGQ1NzYxMWRkYzYxMzhmMTU2NWI1YjAzNjExZTNmNTc2MDAwODA1NDkwNjEwMTAwMGE5MDA0NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTYwMjAwMTkwNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY5MDgxNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTUyNTA1MDYxMWYzYTU2NWI2MDAyNjAwNDgxMTExNTYxMWU1MzU3NjExZTUyNjEzOGYxNTY1YjViODM2MDA0ODExMTE1NjExZTY2NTc2MTFlNjU2MTM4ZjE1NjViNWIwMzYxMWU3OTU3ODE4MTYwNDAwMTgxOTA1MjUwNjExZjM5NTY1YjYwMDM2MDA0ODExMTE1NjExZThkNTc2MTFlOGM2MTM4ZjE1NjViNWI4MzYwMDQ4MTExMTU2MTFlYTA1NzYxMWU5ZjYxMzhmMTU2NWI1YjAzNjExZWIzNTc4MTgxNjA2MDAxODE5MDUyNTA2MTFmMzg1NjViNjAwNDgwODExMTE1NjExZWM2NTc2MTFlYzU2MTM4ZjE1NjViNWI4MzYwMDQ4MTExMTU2MTFlZDk1NzYxMWVkODYxMzhmMTU2NWI1YjAzNjExZjM3NTc2MDAwODA1NDkwNjEwMTAwMGE5MDA0NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTYwODAwMTkwNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY5MDgxNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTUyNTA1MDViNWI1YjViNWI5MjkxNTA1MDU2NWI2MDAwNjAwMTYwMDA4MzYwMDY4MTExMTU2MTFmNWE1NzYxMWY1OTYxMzhmMTU2NWI1YjYwMDY4MTExMTU2MTFmNmM1NzYxMWY2YjYxMzhmMTU2NWI1YjgxNTI2MDIwMDE5MDgxNTI2MDIwMDE2MDAwMjA1NDkwNTA5MTkwNTA1NjViNjExZjhhNjEyMTNlNTY1YjYwMDE2MDA0ODExMTE1NjExZjllNTc2MTFmOWQ2MTM4ZjE1NjViNWI4MzYwMDQ4MTExMTU2MTFmYjE1NzYxMWZiMDYxMzhmMTU2NWI1YjAzNjExZmYzNTc4MTgxNjAyMDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwNjEyMDU4NTY1YjYwMDQ4MDgxMTExNTYxMjAwNjU3NjEyMDA1NjEzOGYxNTY1YjViODM2MDA0ODExMTE1NjEyMDE5NTc2MTIwMTg2MTM4ZjE1NjViNWIwMzYxMjA1NzU3ODE4MTYwODAwMTkwNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY5MDgxNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTUyNTA1MDViNWI5MjkxNTA1MDU2NWI2MDAwODE2MGZmMTY2MDAxOTAxYjgzMTc5MDUwOTI5MTUwNTA1NjViNjA0MDUxODA2MDQwMDE2MDQwNTI4MDYwMDA4MTUyNjAyMDAxNjEyMDhjNjEyMTNlNTY1YjgxNTI1MDkwNTY1YjYwNDA1MTgwNjA2MDAxNjA0MDUyODA2MDAwNjAwNzBiODE1MjYwMjAwMTYwMDA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI2MDIwMDE2MDAwNjAwNzBiODE1MjUwOTA1NjViNjA0MDUxODA2MTAxMjAwMTYwNDA1MjgwNjA2MDgxNTI2MDIwMDE2MDYwODE1MjYwMjAwMTYwMDA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI2MDIwMDE2MDYwODE1MjYwMjAwMTYwMDAxNTE1ODE1MjYwMjAwMTYwMDA2MDA3MGI4MTUyNjAyMDAxNjAwMDE1MTU4MTUyNjAyMDAxNjA2MDgxNTI2MDIwMDE2MTIxMzg2MTIwOTI1NjViODE1MjUwOTA1NjViNjA0MDUxODA2MGEwMDE2MDQwNTI4MDYwMDAxNTE1ODE1MjYwMjAwMTYwMDA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI2MDIwMDE2MDYwODE1MjYwMjAwMTYwNjA4MTUyNjAyMDAxNjAwMDczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2ODE1MjUwOTA=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwoSqvEcNq7d5I2ZGjwXe5DlFJdhuUJQs1a3wmwbldu0Jq9mqo41wsxni50ktpnH3lGgsI8+CerQYQy6i4AiIPCgkItuCerQYQ7QYSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIA"},{"b64Body":"Cg8KCQi34J6tBhDzBhICGAISAhgDGPbR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxjtByKAIDU2NWI2MDAwNjA0MDUxOTA1MDkwNTY1YjYwMDA4MGZkNWI2MDAwODBmZDViNjAwMDczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjgyMTY5MDUwOTE5MDUwNTY1YjYwMDA2MTIxZGE4MjYxMjFhZjU2NWI5MDUwOTE5MDUwNTY1YjYxMjFlYTgxNjEyMWNmNTY1YjgxMTQ2MTIxZjU1NzYwMDA4MGZkNWI1MDU2NWI2MDAwODEzNTkwNTA2MTIyMDc4MTYxMjFlMTU2NWI5MjkxNTA1MDU2NWI2MDAwODE5MDUwOTE5MDUwNTY1YjYxMjIyMDgxNjEyMjBkNTY1YjgxMTQ2MTIyMmI1NzYwMDA4MGZkNWI1MDU2NWI2MDAwODEzNTkwNTA2MTIyM2Q4MTYxMjIxNzU2NWI5MjkxNTA1MDU2NWI2MDAwODA2MDAwODA2MDgwODU4NzAzMTIxNTYxMjI1ZDU3NjEyMjVjNjEyMWE1NTY1YjViNjAwMDYxMjI2Yjg3ODI4ODAxNjEyMWY4NTY1Yjk0NTA1MDYwMjA2MTIyN2M4NzgyODgwMTYxMjFmODU2NWI5MzUwNTA2MDQwNjEyMjhkODc4Mjg4MDE2MTIxZjg1NjViOTI1MDUwNjA2MDYxMjI5ZTg3ODI4ODAxNjEyMjJlNTY1YjkxNTA1MDkyOTU5MTk0NTA5MjUwNTY1YjYwMDA4MTYwMDcwYjkwNTA5MTkwNTA1NjViNjEyMmMwODE2MTIyYWE1NjViODI1MjUwNTA1NjViNjAwMDYwMjA4MjAxOTA1MDYxMjJkYjYwMDA4MzAxODQ2MTIyYjc1NjViOTI5MTUwNTA1NjViNjAwMDgwZmQ1YjYwMDA4MGZkNWI2MDAwNjAxZjE5NjAxZjgzMDExNjkwNTA5MTkwNTA1NjViN2Y0ZTQ4N2I3MTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDUyNjA0MTYwMDQ1MjYwMjQ2MDAwZmQ1YjYxMjMzNDgyNjEyMmViNTY1YjgxMDE4MTgxMTA2N2ZmZmZmZmZmZmZmZmZmZmY4MjExMTcxNTYxMjM1MzU3NjEyMzUyNjEyMmZjNTY1YjViODA2MDQwNTI1MDUwNTA1NjViNjAwMDYxMjM2NjYxMjE5YjU2NWI5MDUwNjEyMzcyODI4MjYxMjMyYjU2NWI5MTkwNTA1NjViNjAwMDY3ZmZmZmZmZmZmZmZmZmZmZjgyMTExNTYxMjM5MjU3NjEyMzkxNjEyMmZjNTY1YjViNjEyMzliODI2MTIyZWI1NjViOTA1MDYwMjA4MTAxOTA1MDkxOTA1MDU2NWI4MjgxODMzNzYwMDA4MzgzMDE1MjUwNTA1MDU2NWI2MDAwNjEyM2NhNjEyM2M1ODQ2MTIzNzc1NjViNjEyMzVjNTY1YjkwNTA4MjgxNTI2MDIwODEwMTg0ODQ4NDAxMTExNTYxMjNlNjU3NjEyM2U1NjEyMmU2NTY1YjViNjEyM2YxODQ4Mjg1NjEyM2E4NTY1YjUwOTM5MjUwNTA1MDU2NWI2MDAwODI2MDFmODMwMTEyNjEyNDBlNTc2MTI0MGQ2MTIyZTE1NjViNWI4MTM1NjEyNDFlODQ4MjYwMjA4NjAxNjEyM2I3NTY1YjkxNTA1MDkyOTE1MDUwNTY1YjYxMjQzMDgxNjEyMmFhNTY1YjgxMTQ2MTI0M2I1NzYwMDA4MGZkNWI1MDU2NWI2MDAwODEzNTkwNTA2MTI0NGQ4MTYxMjQyNzU2NWI5MjkxNTA1MDU2NWI2MDAwODA2MDAwODA2MDAwODA2MDAwNjBlMDg4OGEwMzEyMTU2MTI0NzI1NzYxMjQ3MTYxMjFhNTU2NWI1YjYwMDA2MTI0ODA4YTgyOGIwMTYxMjFmODU2NWI5NzUwNTA2MDIwNjEyNDkxOGE4MjhiMDE2MTIxZjg1NjViOTY1MDUwNjA0MDg4MDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjRiMjU3NjEyNGIxNjEyMWFhNTY1YjViNjEyNGJlOGE4MjhiMDE2MTIzZjk1NjViOTU1MDUwNjA2MDg4MDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjRkZjU3NjEyNGRlNjEyMWFhNTY1YjViNjEyNGViOGE4MjhiMDE2MTIzZjk1NjViOTQ1MDUwNjA4MDYxMjRmYzhhODI4YjAxNjEyMWY4NTY1YjkzNTA1MDYwYTA2MTI1MGQ4YTgyOGIwMTYxMjFmODU2NWI5MjUwNTA2MGMwNjEyNTFlOGE4MjhiMDE2MTI0M2U1NjViOTE1MDUwOTI5NTk4OTE5NDk3NTA5Mjk1NTA1NjViNjAwMDgwNjA0MDgzODUwMzEyMTU2MTI1NDQ1NzYxMjU0MzYxMjFhNTU2NWI1YjYwMDA2MTI1NTI4NTgyODYwMTYxMjFmODU2NWI5MjUwNTA2MDIwNjEyNTYzODU4Mjg2MDE2MTIyMmU1NjViOTE1MDUwOTI1MDkyOTA1MDU2NWI2MDAwODExNTE1OTA1MDkxOTA1MDU2NWI2MTI1ODI4MTYxMjU2ZDU2NWI4MjUyNTA1MDU2NWI2MTI1OTE4MTYxMjFjZjU2NWI4MjUyNTA1MDU2NWI2MDAwODE1MTkwNTA5MTkwNTA1NjViNjAwMDgyODI1MjYwMjA4MjAxOTA1MDkyOTE1MDUwNTY1YjYwMDA1YjgzODExMDE1NjEyNWQxNTc4MDgyMDE1MTgxODQwMTUyNjAyMDgxMDE5MDUwNjEyNWI2NTY1YjYwMDA4NDg0MDE1MjUwNTA1MDUwNTY1YjYwMDA2MTI1ZTg4MjYxMjU5NzU2NWI2MTI1ZjI4MTg1NjEyNWEyNTY1YjkzNTA2MTI2MDI4MTg1NjAyMDg2MDE2MTI1YjM1NjViNjEyNjBiODE2MTIyZWI1NjViODQwMTkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MGEwODMwMTYwMDA4MzAxNTE2MTI2MmU2MDAwODYwMTgyNjEyNTc5NTY1YjUwNjAyMDgzMDE1MTYxMjY0MTYwMjA4NjAxODI2MTI1ODg1NjViNTA2MDQwODMwMTUxODQ4MjAzNjA0MDg2MDE1MjYxMjY1OTgyODI2MTI1ZGQ1NjViOTE1MDUwNjA2MDgzMDE1MTg0ODIwMzYwNjA4NjAxNTI2MTI2NzM4MjgyNjEyNWRkNTY1YjkxNTA1MDYwODA4MzAxNTE2MTI2ODg2MDgwODYwMTgyNjEyNTg4NTY1YjUwODA5MTUwNTA5MjkxNTA1MDU2NWI2MDAwNjAyMDgyMDE5MDUwODE4MTAzNjAwMDgzMDE1MjYxMjZhZDgxODQ2MTI2MTY1NjViOTA1MDkyOTE1MDUwNTY1YjYwMDA4MDYwNDA4Mzg1MDMxMjE1NjEyNmNjNTc2MTI2Y2I2MTIxYTU1NjViNWI2MDAwNjEyNmRhODU4Mjg2MDE2MTIxZjg1NjViOTI1MDUwNjAyMDgzMDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjZmYjU3NjEyNmZhNjEyMWFhNTY1YjViNjEyNzA3ODU4Mjg2MDE2MTIzZjk1NjViOTE1MDUwOTI1MDkyOTA1MDU2NWI2MDAwODA2MDAwODA2MDgwODU4NzAzMTIxNTYxMjcyYjU3NjEyNzJhNjEyMWE1NTY1YjViNjAwMDYxMjczOTg3ODI4ODAxNjEyMWY4NTY1Yjk0NTA1MDYwMjA4NTAxMzU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTI3NWE1NzYxMjc1OTYxMjFhYTU2NWI1YjYxMjc2Njg3ODI4ODAxNjEyM2Y5NTY1YjkzNTA1MDYwNDA4NTAxMzU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTI3ODc1NzYxMjc4NjYxMjFhYTU2NWI1YjYxMjc5Mzg3ODI4ODAxNjEyM2Y5NTY1YjkyNTA1MDYwNjA2MTI3YTQ4NzgyODgwMTYxMjFmODU2NWI5MTUwNTA5Mjk1OTE5NDUwOTI1MDU2NWI2MDAwODA2MDAwNjA2MDg0ODYwMzEyMTU2MTI3Yzk1NzYxMjdjODYxMjFhNTU2NWI1YjYwMDA2MTI3ZDc4NjgyODcwMTYxMjFmODU2NWI5MzUwNTA2MDIwNjEyN2U4ODY4Mjg3MDE2MTIxZjg1NjViOTI1MDUwNjA0MDYxMjdmOTg2ODI4NzAxNjEyNDNlNTY1YjkxNTA1MDkyNTA5MjUwOTI1NjViNjAwMDY3ZmZmZmZmZmZmZmZmZmZmZjgyMTExNTYxMjgxZTU3NjEyODFkNjEyMmZjNTY1YjViNjEyODI3ODI2MTIyZWI1NjViOTA1MDYwMjA4MTAxOTA1MDkxOTA1MDU2NWI2MDAwNjEyODQ3NjEyODQyODQ2MTI4MDM1NjViNjEyMzVjNTY1YjkwNTA4MjgxNTI2MDIwODEwMTg0ODQ4NDAxMTExNTYxMjg2MzU3NjEyODYyNjEyMmU2NTY1YjViNjEyODZlODQ4Mjg1NjEyM2E4NTY1YjUwOTM5MjUwNTA1MDU2NWI2MDAwODI2MDFmODMwMTEyNjEyODhiNTc2MTI4OGE2MTIyZTE1NjViNWI4MTM1NjEyODliODQ4MjYwMjA4NjAxNjEyODM0NTY1YjkxNTA1MDkyOTE1MDUwNTY1YjYwMDA4MDYwMDA4MDYwMDA4MDYwMDA4MDYwMDA4MDYxMDE0MDhiOGQwMzEyMTU2MTI4Yzg1NzYxMjhjNzYxMjFhNTU2NWI1YjYwMDA2MTI4ZDY4ZDgyOGUwMTYxMjFmODU2NWI5YTUwNTA2MDIwNjEyOGU3OGQ4MjhlMDE2MTIxZjg1NjViOTk1MDUwNjA0MDhiMDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjkwODU3NjEyOTA3NjEyMWFhNTY1YjViNjEyOTE0OGQ4MjhlMDE2MTIzZjk1NjViOTg1MDUwNjA2MDhiMDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjkzNTU3NjEyOTM0NjEyMWFhNTY1YjViNjEyOTQxOGQ4MjhlMDE2MTIzZjk1NjViOTc1MDUwNjA4MDYxMjk1MjhkODI4ZTAxNjEyMWY4NTY1Yjk2NTA1MDYwYTA2MTI5NjM4ZDgyOGUwMTYxMjFmODU2NWI5NTUwNTA2MGMwNjEyOTc0OGQ4MjhlMDE2MTI0M2U1NjViOTQ1MDUwNjBlMDhiMDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjk5NTU3NjEyOTk0NjEyMWFhNTY1YjViNjEyOWExOGQ=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwEgtSOzk/SfDF/Q0B2c4VIvQDj6kvNw7BI0EtANGYflhOyPKzX7hvDkinNG7hS8GmGgwI8+CerQYQ29/7hAIiDwoJCLfgnq0GEPMGEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQi44J6tBhD5BhICGAISAhgDGPbR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxjtByKAIDgyOGUwMTYxMjg3NjU2NWI5MzUwNTA2MTAxMDA4YjAxMzU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTI5YzM1NzYxMjljMjYxMjFhYTU2NWI1YjYxMjljZjhkODI4ZTAxNjEyODc2NTY1YjkyNTA1MDYxMDEyMDhiMDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjlmMTU3NjEyOWYwNjEyMWFhNTY1YjViNjEyOWZkOGQ4MjhlMDE2MTI4NzY1NjViOTE1MDUwOTI5NTk4OWI5MTk0OTc5YTUwOTI5NTk4NTA1NjViNjAwMDgwNjAwMDgwNjAwMDYwYTA4Njg4MDMxMjE1NjEyYTJiNTc2MTJhMmE2MTIxYTU1NjViNWI2MDAwNjEyYTM5ODg4Mjg5MDE2MTIxZjg1NjViOTU1MDUwNjAyMDYxMmE0YTg4ODI4OTAxNjEyMWY4NTY1Yjk0NTA1MDYwNDA4NjAxMzU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTJhNmI1NzYxMmE2YTYxMjFhYTU2NWI1YjYxMmE3Nzg4ODI4OTAxNjEyM2Y5NTY1YjkzNTA1MDYwNjA4NjAxMzU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTJhOTg1NzYxMmE5NzYxMjFhYTU2NWI1YjYxMmFhNDg4ODI4OTAxNjEyM2Y5NTY1YjkyNTA1MDYwODA2MTJhYjU4ODgyODkwMTYxMjFmODU2NWI5MTUwNTA5Mjk1NTA5Mjk1OTA5MzUwNTY1YjYwMDA4MDYwNDA4Mzg1MDMxMjE1NjEyYWQ5NTc2MTJhZDg2MTIxYTU1NjViNWI2MDAwNjEyYWU3ODU4Mjg2MDE2MTIxZjg1NjViOTI1MDUwNjAyMDYxMmFmODg1ODI4NjAxNjEyMWY4NTY1YjkxNTA1MDkyNTA5MjkwNTA1NjViNjAwMDgwNjAwMDgwNjA4MDg1ODcwMzEyMTU2MTJiMWM1NzYxMmIxYjYxMjFhNTU2NWI1YjYwMDA2MTJiMmE4NzgyODgwMTYxMjFmODU2NWI5NDUwNTA2MDIwNjEyYjNiODc4Mjg4MDE2MTIxZjg1NjViOTM1MDUwNjA0MDg1MDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMmI1YzU3NjEyYjViNjEyMWFhNTY1YjViNjEyYjY4ODc4Mjg4MDE2MTI4NzY1NjViOTI1MDUwNjA2MDg1MDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMmI4OTU3NjEyYjg4NjEyMWFhNTY1YjViNjEyYjk1ODc4Mjg4MDE2MTI4NzY1NjViOTE1MDUwOTI5NTkxOTQ1MDkyNTA1NjViNjEyYmFhODE2MTIxY2Y1NjViODI1MjUwNTA1NjViNjEyYmI5ODE2MTIyMGQ1NjViODI1MjUwNTA1NjViNjAwMDYwODA4MjAxOTA1MDYxMmJkNDYwMDA4MzAxODc2MTJiYTE1NjViNjEyYmUxNjAyMDgzMDE4NjYxMmJhMTU2NWI2MTJiZWU2MDQwODMwMTg1NjEyYmExNTY1YjYxMmJmYjYwNjA4MzAxODQ2MTJiYjA1NjViOTU5NDUwNTA1MDUwNTA1NjViNjAwMDgxOTA1MDkyOTE1MDUwNTY1YjYwMDA2MTJjMWE4MjYxMjU5NzU2NWI2MTJjMjQ4MTg1NjEyYzA0NTY1YjkzNTA2MTJjMzQ4MTg1NjAyMDg2MDE2MTI1YjM1NjViODA4NDAxOTE1MDUwOTI5MTUwNTA1NjViNjAwMDYxMmM0YzgyODQ2MTJjMGY1NjViOTE1MDgxOTA1MDkyOTE1MDUwNTY1YjYwMDA4MTYwMDMwYjkwNTA5MTkwNTA1NjViNjEyYzZkODE2MTJjNTc1NjViODExNDYxMmM3ODU3NjAwMDgwZmQ1YjUwNTY1YjYwMDA4MTUxOTA1MDYxMmM4YTgxNjEyYzY0NTY1YjkyOTE1MDUwNTY1YjYwMDA2MDIwODI4NDAzMTIxNTYxMmNhNjU3NjEyY2E1NjEyMWE1NTY1YjViNjAwMDYxMmNiNDg0ODI4NTAxNjEyYzdiNTY1YjkxNTA1MDkyOTE1MDUwNTY1YjdmNGU0ODdiNzEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDYwMDA1MjYwMzI2MDA0NTI2MDI0NjAwMGZkNWI2MDAwODI4MjUyNjAyMDgyMDE5MDUwOTI5MTUwNTA1NjViN2Y1NTcwNjQ2MTc0NjUyMDZmNjYyMDc0NmY2YjY1NmU0OTZlNjY2ZjIwNjY2MTY5NmM2NTY0MjEwMDAwMDAwMDAwNjAwMDgyMDE1MjUwNTY1YjYwMDA2MTJkMzM2MDFiODM2MTJjZWM1NjViOTE1MDYxMmQzZTgyNjEyY2ZkNTY1YjYwMjA4MjAxOTA1MDkxOTA1MDU2NWI2MDAwNjAyMDgyMDE5MDUwODE4MTAzNjAwMDgzMDE1MjYxMmQ2MjgxNjEyZDI2NTY1YjkwNTA5MTkwNTA1NjViNjAwMDgyODI1MjYwMjA4MjAxOTA1MDkyOTE1MDUwNTY1YjYwMDA2MTJkODU4MjYxMjU5NzU2NWI2MTJkOGY4MTg1NjEyZDY5NTY1YjkzNTA2MTJkOWY4MTg1NjAyMDg2MDE2MTI1YjM1NjViNjEyZGE4ODE2MTIyZWI1NjViODQwMTkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MDQwODIwMTkwNTA2MTJkYzg2MDAwODMwMTg1NjEyYmExNTY1YjgxODEwMzYwMjA4MzAxNTI2MTJkZGE4MTg0NjEyZDdhNTY1YjkwNTA5MzkyNTA1MDUwNTY1YjYxMmRlYzgxNjEyNTZkNTY1YjgyNTI1MDUwNTY1YjYwMDA2MDQwODIwMTkwNTA2MTJlMDc2MDAwODMwMTg1NjEyZGUzNTY1YjgxODEwMzYwMjA4MzAxNTI2MTJlMTk4MTg0NjEyZDdhNTY1YjkwNTA5MzkyNTA1MDUwNTY1YjdmNTU3MDY0NjE3NDY1MjA2ZjY2MjA3NDZmNmI2NTZlMjA2YjY1Nzk3MzIwNjY2MTY5NmM2NTY0MjEwMDAwMDAwMDYwMDA4MjAxNTI1MDU2NWI2MDAwNjEyZTU4NjAxYzgzNjEyY2VjNTY1YjkxNTA2MTJlNjM4MjYxMmUyMjU2NWI2MDIwODIwMTkwNTA5MTkwNTA1NjViNjAwMDYwMjA4MjAxOTA1MDgxODEwMzYwMDA4MzAxNTI2MTJlODc4MTYxMmU0YjU2NWI5MDUwOTE5MDUwNTY1YjYwMDA4MTUxOTA1MDkxOTA1MDU2NWI3ZjRlNDg3YjcxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA2MDAwNTI2MDIyNjAwNDUyNjAyNDYwMDBmZDViNjAwMDYwMDI4MjA0OTA1MDYwMDE4MjE2ODA2MTJlZTA1NzYwN2Y4MjE2OTE1MDViNjAyMDgyMTA4MTAzNjEyZWYzNTc2MTJlZjI2MTJlOTk1NjViNWI1MDkxOTA1MDU2NWI2MDAwODE5MDUwODE2MDAwNTI2MDIwNjAwMDIwOTA1MDkxOTA1MDU2NWI2MDAwNjAyMDYwMWY4MzAxMDQ5MDUwOTE5MDUwNTY1YjYwMDA4MjgyMWI5MDUwOTI5MTUwNTA1NjViNjAwMDYwMDg4MzAyNjEyZjViN2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmODI2MTJmMWU1NjViNjEyZjY1ODY4MzYxMmYxZTU2NWI5NTUwODAxOTg0MTY5MzUwODA4NjE2ODQxNzkyNTA1MDUwOTM5MjUwNTA1MDU2NWI2MDAwODE5MDUwOTE5MDUwNTY1YjYwMDA2MTJmYTI2MTJmOWQ2MTJmOTg4NDYxMjIwZDU2NWI2MTJmN2Q1NjViNjEyMjBkNTY1YjkwNTA5MTkwNTA1NjViNjAwMDgxOTA1MDkxOTA1MDU2NWI2MTJmYmM4MzYxMmY4NzU2NWI2MTJmZDA2MTJmYzg4MjYxMmZhOTU2NWI4NDg0NTQ2MTJmMmI1NjViODI1NTUwNTA1MDUwNTY1YjYwMDA5MDU2NWI2MTJmZTU2MTJmZDg1NjViNjEyZmYwODE4NDg0NjEyZmIzNTY1YjUwNTA1MDU2NWI1YjgxODExMDE1NjEzMDE0NTc2MTMwMDk2MDAwODI2MTJmZGQ1NjViNjAwMTgxMDE5MDUwNjEyZmY2NTY1YjUwNTA1NjViNjAxZjgyMTExNTYxMzA1OTU3NjEzMDJhODE2MTJlZjk1NjViNjEzMDMzODQ2MTJmMGU1NjViODEwMTYwMjA4NTEwMTU2MTMwNDI1NzgxOTA1MDViNjEzMDU2NjEzMDRlODU2MTJmMGU1NjViODMwMTgyNjEyZmY1NTY1YjUwNTA1YjUwNTA1MDU2NWI2MDAwODI4MjFjOTA1MDkyOTE1MDUwNTY1YjYwMDA2MTMwN2M2MDAwMTk4NDYwMDgwMjYxMzA1ZTU2NWIxOTgwODMxNjkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MTMwOTU4MzgzNjEzMDZiNTY1YjkxNTA4MjYwMDIwMjgyMTc5MDUwOTI5MTUwNTA1NjViNjEzMGFlODI2MTJlOGU1NjViNjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEzMGM3NTc2MTMwYzY2MTIyZmM1NjViNWI2MTMwZDE4MjU0NjEyZWM4NTY1YjYxMzBkYzgyODI4NTYxMzAxODU2NWI2MDAwNjAyMDkwNTA2MDFmODMxMTYwMDE4MTE0NjEzMTBmNTc2MDAwODQxNTYxMzBmZDU3ODI4NzAxNTE5MDUwNWI2MTMxMDc4NTgyNjEzMDg5NTY1Yjg2NTU1MDYxMzE2ZjU2NWI2MDFmMTk4NDE2NjEzMTFkODY2MTJlZjk1NjViNjAwMDViODI4MTEwMTU2MTMxNDU1Nzg0ODkwMTUxODI1NTYwMDE4MjAxOTE1MDYwMjA4NTAxOTQ1MDYwMjA4MTAxOTA1MDYxMzEyMDU2NWI4NjgzMTAxNTYxMzE2MjU3ODQ4OTAxNTE2MTMxNWU2MDFmODkxNjgyNjEzMDZiNTY1YjgzNTU1MDViNjAwMTYwMDI4ODAyMDE4ODU1NTA1MDUwNWI1MDUwNTA1MDUwNTA1NjViN2Y1NTcwNjQ2MTc0NjUyMDZmNjYyMDc0NmY2YjY1NmU0OTZlNjY2ZjIwNmI2NTc5NzMyMDY2NjE2OTZjNjU2NDIxNjA=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwXcaiZCX0kAmcl1TGtHrdXiMiftMxIhteMoZAkEgTxD+R2zhIr6fuoiLnTLGHmk3AGgsI9OCerQYQ247pGSIPCgkIuOCerQYQ+QYSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIA"},{"b64Body":"Cg8KCQi44J6tBhD/BhICGAISAhgDGPKjnj4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBgB8SAxjtByL4HjAwODIwMTUyNTA1NjViNjAwMDYxMzFhZDYwMjA4MzYxMmNlYzU2NWI5MTUwNjEzMWI4ODI2MTMxNzc1NjViNjAyMDgyMDE5MDUwOTE5MDUwNTY1YjYwMDA2MDIwODIwMTkwNTA4MTgxMDM2MDAwODMwMTUyNjEzMWRjODE2MTMxYTA1NjViOTA1MDkxOTA1MDU2NWI3ZjU1NzA2NDYxNzQ2NTIwNmY2NjIwNzQ2ZjZiNjU2ZTQ5NmU2NjZmMmU3NDcyNjU2MTczNzU3Mjc5MjA2NjYxNjk2MDAwODIwMTUyN2Y2YzY1NjQyMTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAyMDgyMDE1MjUwNTY1YjYwMDA2MTMyM2Y2MDI0ODM2MTJjZWM1NjViOTE1MDYxMzI0YTgyNjEzMWUzNTY1YjYwNDA4MjAxOTA1MDkxOTA1MDU2NWI2MDAwNjAyMDgyMDE5MDUwODE4MTAzNjAwMDgzMDE1MjYxMzI2ZTgxNjEzMjMyNTY1YjkwNTA5MTkwNTA1NjViN2Y1NTcwNjQ2MTc0NjUyMDZmNjYyMDc0NmY2YjY1NmU0OTZlNjY2ZjIwNmU2MTZkNjUyMDYxNmU2NDIwNzM3OTZkNjAwMDgyMDE1MjdmNjI2ZjZjMjA2NjYxNjk2YzY1NjQyMTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDYwMjA4MjAxNTI1MDU2NWI2MDAwNjEzMmQxNjAyYjgzNjEyY2VjNTY1YjkxNTA2MTMyZGM4MjYxMzI3NTU2NWI2MDQwODIwMTkwNTA5MTkwNTA1NjViNjAwMDYwMjA4MjAxOTA1MDgxODEwMzYwMDA4MzAxNTI2MTMzMDA4MTYxMzJjNDU2NWI5MDUwOTE5MDUwNTY1YjYwMDA4MjgyNTI2MDIwODIwMTkwNTA5MjkxNTA1MDU2NWI2MDAwNjEzMzIzODI2MTJlOGU1NjViNjEzMzJkODE4NTYxMzMwNzU2NWI5MzUwNjEzMzNkODE4NTYwMjA4NjAxNjEyNWIzNTY1YjYxMzM0NjgxNjEyMmViNTY1Yjg0MDE5MTUwNTA5MjkxNTA1MDU2NWI2MTMzNWE4MTYxMjJhYTU2NWI4MjUyNTA1MDU2NWI2MDAwODE1MTkwNTA5MTkwNTA1NjViNjAwMDgyODI1MjYwMjA4MjAxOTA1MDkyOTE1MDUwNTY1YjYwMDA4MTkwNTA2MDIwODIwMTkwNTA5MTkwNTA1NjViNjEzMzk1ODE2MTIyMGQ1NjViODI1MjUwNTA1NjViNjAwMDYwYTA4MzAxNjAwMDgzMDE1MTYxMzNiMzYwMDA4NjAxODI2MTI1Nzk1NjViNTA2MDIwODMwMTUxNjEzM2M2NjAyMDg2MDE4MjYxMjU4ODU2NWI1MDYwNDA4MzAxNTE4NDgyMDM2MDQwODYwMTUyNjEzM2RlODI4MjYxMjVkZDU2NWI5MTUwNTA2MDYwODMwMTUxODQ4MjAzNjA2MDg2MDE1MjYxMzNmODgyODI2MTI1ZGQ1NjViOTE1MDUwNjA4MDgzMDE1MTYxMzQwZDYwODA4NjAxODI2MTI1ODg1NjViNTA4MDkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MDQwODMwMTYwMDA4MzAxNTE2MTM0MzA2MDAwODYwMTgyNjEzMzhjNTY1YjUwNjAyMDgzMDE1MTg0ODIwMzYwMjA4NjAxNTI2MTM0NDg4MjgyNjEzMzliNTY1YjkxNTA1MDgwOTE1MDUwOTI5MTUwNTA1NjViNjAwMDYxMzQ2MTgzODM2MTM0MTg1NjViOTA1MDkyOTE1MDUwNTY1YjYwMDA2MDIwODIwMTkwNTA5MTkwNTA1NjViNjAwMDYxMzQ4MTgyNjEzMzYwNTY1YjYxMzQ4YjgxODU2MTMzNmI1NjViOTM1MDgzNjAyMDgyMDI4NTAxNjEzNDlkODU2MTMzN2M1NjViODA2MDAwNWI4NTgxMTAxNTYxMzRkOTU3ODQ4NDAzODk1MjgxNTE2MTM0YmE4NTgyNjEzNDU1NTY1Yjk0NTA2MTM0YzU4MzYxMzQ2OTU2NWI5MjUwNjAyMDhhMDE5OTUwNTA2MDAxODEwMTkwNTA2MTM0YTE1NjViNTA4Mjk3NTA4Nzk1NTA1MDUwNTA1MDUwOTI5MTUwNTA1NjViNjA2MDgyMDE2MDAwODIwMTUxNjEzNTAxNjAwMDg1MDE4MjYxMzM1MTU2NWI1MDYwMjA4MjAxNTE2MTM1MTQ2MDIwODUwMTgyNjEyNTg4NTY1YjUwNjA0MDgyMDE1MTYxMzUyNzYwNDA4NTAxODI2MTMzNTE1NjViNTA1MDUwNTA1NjViNjAwMDYxMDE2MDgzMDE2MDAwODMwMTUxODQ4MjAzNjAwMDg2MDE1MjYxMzU0YjgyODI2MTMzMTg1NjViOTE1MDUwNjAyMDgzMDE1MTg0ODIwMzYwMjA4NjAxNTI2MTM1NjU4MjgyNjEzMzE4NTY1YjkxNTA1MDYwNDA4MzAxNTE2MTM1N2E2MDQwODYwMTgyNjEyNTg4NTY1YjUwNjA2MDgzMDE1MTg0ODIwMzYwNjA4NjAxNTI2MTM1OTI4MjgyNjEzMzE4NTY1YjkxNTA1MDYwODA4MzAxNTE2MTM1YTc2MDgwODYwMTgyNjEyNTc5NTY1YjUwNjBhMDgzMDE1MTYxMzViYTYwYTA4NjAxODI2MTMzNTE1NjViNTA2MGMwODMwMTUxNjEzNWNkNjBjMDg2MDE4MjYxMjU3OTU2NWI1MDYwZTA4MzAxNTE4NDgyMDM2MGUwODYwMTUyNjEzNWU1ODI4MjYxMzQ3NjU2NWI5MTUwNTA2MTAxMDA4MzAxNTE2MTM1ZmM2MTAxMDA4NjAxODI2MTM0ZWI1NjViNTA4MDkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MDQwODIwMTkwNTA2MTM2MWM2MDAwODMwMTg1NjEyYmExNTY1YjgxODEwMzYwMjA4MzAxNTI2MTM2MmU4MTg0NjEzNTJkNTY1YjkwNTA5MzkyNTA1MDUwNTY1YjYwMDA2MDQwODIwMTkwNTA2MTM2NGM2MDAwODMwMTg1NjEyYmExNTY1YjYxMzY1OTYwMjA4MzAxODQ2MTJiYjA1NjViOTM5MjUwNTA1MDU2NWI2MDAwODBmZDViNjAwMDgwZmQ1YjYxMzY3MzgxNjEyNTZkNTY1YjgxMTQ2MTM2N2U1NzYwMDA4MGZkNWI1MDU2NWI2MDAwODE1MTkwNTA2MTM2OTA4MTYxMzY2YTU2NWI5MjkxNTA1MDU2NWI2MDAwODE1MTkwNTA2MTM2YTU4MTYxMjFlMTU2NWI5MjkxNTA1MDU2NWI2MDAwNjEzNmJlNjEzNmI5ODQ2MTIzNzc1NjViNjEyMzVjNTY1YjkwNTA4MjgxNTI2MDIwODEwMTg0ODQ4NDAxMTExNTYxMzZkYTU3NjEzNmQ5NjEyMmU2NTY1YjViNjEzNmU1ODQ4Mjg1NjEyNWIzNTY1YjUwOTM5MjUwNTA1MDU2NWI2MDAwODI2MDFmODMwMTEyNjEzNzAyNTc2MTM3MDE2MTIyZTE1NjViNWI4MTUxNjEzNzEyODQ4MjYwMjA4NjAxNjEzNmFiNTY1YjkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MGEwODI4NDAzMTIxNTYxMzczMTU3NjEzNzMwNjEzNjYwNTY1YjViNjEzNzNiNjBhMDYxMjM1YzU2NWI5MDUwNjAwMDYxMzc0Yjg0ODI4NTAxNjEzNjgxNTY1YjYwMDA4MzAxNTI1MDYwMjA2MTM3NWY4NDgyODUwMTYxMzY5NjU2NWI2MDIwODMwMTUyNTA2MDQwODIwMTUxNjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEzNzgzNTc2MTM3ODI2MTM2NjU1NjViNWI2MTM3OGY4NDgyODUwMTYxMzZlZDU2NWI2MDQwODMwMTUyNTA2MDYwODIwMTUxNjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEzN2IzNTc2MTM3YjI2MTM2NjU1NjViNWI2MTM3YmY4NDgyODUwMTYxMzZlZDU2NWI2MDYwODMwMTUyNTA2MDgwNjEzN2QzODQ4Mjg1MDE2MTM2OTY1NjViNjA4MDgzMDE1MjUwOTI5MTUwNTA1NjViNjAwMDgwNjA0MDgzODUwMzEyMTU2MTM3ZjY1NzYxMzdmNTYxMjFhNTU2NWI1YjYwMDA2MTM4MDQ4NTgyODYwMTYxMmM3YjU2NWI5MjUwNTA2MDIwODMwMTUxNjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEzODI1NTc2MTM4MjQ2MTIxYWE1NjViNWI2MTM4MzE4NTgyODYwMTYxMzcxYjU2NWI5MTUwNTA5MjUwOTI5MDUwNTY1YjYwMDA4MjgyNTI2MDIwODIwMTkwNTA5MjkxNTA1MDU2NWI2MDAwNjEzODU3ODI2MTMzNjA1NjViNjEzODYxODE4NTYxMzgzYjU2NWI5MzUwODM2MDIwODIwMjg1MDE2MTM4NzM4NTYxMzM3YzU2NWI4MDYwMDA1Yjg1ODExMDE1NjEzOGFmNTc4NDg0MDM4OTUyODE1MTYxMzg5MDg1ODI2MTM0NTU1NjViOTQ1MDYxMzg5YjgzNjEzNDY5NTY1YjkyNTA2MDIwOGEwMTk5NTA1MDYwMDE4MTAxOTA1MDYxMzg3NzU2NWI1MDgyOTc1MDg3OTU1MDUwNTA1MDUwNTA5MjkxNTA1MDU2NWI2MDAwNjA0MDgyMDE5MDUwNjEzOGQ2NjAwMDgzMDE4NTYxMmJhMTU2NWI4MTgxMDM2MDIwODMwMTUyNjEzOGU4ODE4NDYxMzg0YzU2NWI5MDUwOTM5MjUwNTA1MDU2NWI3ZjRlNDg3YjcxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA2MDAwNTI2MDIxNjAwNDUyNjAyNDYwMDBmZGZlYTI2NDY5NzA2NjczNTgyMjEyMjBlZDAxNzAwYmU1OTIxNmVjMDMxNjViOGEyMWZkOTVjY2NmOTQ3NmM1NmI5M2FiNTk5Nzg3MmE4ZGZkZWNiYTY0NjQ3MzZmNmM2MzQzMDAwODEyMDAzMw==","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwLbUu/cWeSf7+tiEe4IH0+QrxUjkFMFpnVMg1VAddWoJDQMbS/2fKKnV//zN2Z2m1GgwI9OCerQYQs+m2oAIiDwoJCLjgnq0GEP8GEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQi54J6tBhCBBxICGAISAhgDGJb7rp0CIgIIeDIgw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5CRgoDGO0HGiISIPOON+A+dAtymE/Vh9tBKZk6v4CjVKqdaFruNt5DLR6jIICS9AFCBQiAztoDUgBaAGoLY2VsbGFyIGRvb3I=","b64Record":"CiUIFiIDGO4HKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjA/DbyGSzCs79SistfmkVB+6hSSi0qkzLGXIXBIV4OjI+T3/JlbFtoi76+kwOPyRcEaCwj14J6tBhCDxr4pIg8KCQi54J6tBhCBBxICGAIqIMODwq7DgsK3w4PCuXRGOMOCwq5Kw4PCi8ODwpDDg8KOMICYq2xCjHUKAxjuBxLWcmCAYEBSYAQ2EGEAwldgADVg4ByAY33LvHIRYQB/V4Bj6sbz/hFhAFlXgGPqxvP+FGECVFeAY+tUjv8UYQKRV4Bj8AcpkBRhAq1XgGPxcwdgFGECyVdhAMJWW4Bjfcu8chRhAd9XgGObI9PZFGEB+1eAY+YI4Y0UYQI4V2EAwlZbgGMR4fwHFGEAx1eAYxXay+oUYQEEV4BjOqEu9RRhAUFXgGNBKbfbFGEBXVeAY2GNxl4UYQGaV4BjeP5yoRRhAcNXW2AAgP1bNIAVYQDTV2AAgP1bUGEA7mAEgDYDgQGQYQDpkZBhIkNWW2EC5VZbYEBRYQD7kZBhIsZWW2BAUYCRA5DzWzSAFWEBEFdgAID9W1BhAStgBIA2A4EBkGEBJpGQYSJDVlthBAFWW2BAUWEBOJGQYSLGVltgQFGAkQOQ81thAVtgBIA2A4EBkGEBVpGQYSRTVlthBR9WWwBbNIAVYQFpV2AAgP1bUGEBhGAEgDYDgQGQYQF/kZBhJS1WW2EHcFZbYEBRYQGRkZBhJpNWW2BAUYCRA5DzWzSAFWEBpldgAID9W1BhAcFgBIA2A4EBkGEBvJGQYSa1VlthB6dWWwBbYQHdYASANgOBAZBhAdiRkGEnEVZbYQjOVlsAW2EB+WAEgDYDgQGQYQH0kZBhJ7BWW2EKZVZbAFs0gBVhAgdXYACA/VtQYQIiYASANgOBAZBhAh2RkGEiQ1ZbYQuFVltgQFFhAi+RkGEixlZbYEBRgJEDkPNbYQJSYASANgOBAZBhAk2RkGEopFZbYQyjVlsAWzSAFWECYFdgAID9W1BhAntgBIA2A4EBkGECdpGQYSJDVlthDoBWW2BAUWECiJGQYSLGVltgQFGAkQOQ81thAqtgBIA2A4EBkGECppGQYSoPVlthD5xWWwBbYQLHYASANgOBAZBhAsKRkGEqwlZbYRM0VlsAW2EC42AEgDYDgQGQYQLekZBhKwJWW2EVh1ZbAFtgAIBgAGEBZ3P//////////////////////////xZjmyPT2WDgG4iIiIhgQFFgJAFhAyKUk5KRkGErv1ZbYEBRYCCBgwMDgVKQYEBSkHv/////////////////////////////////////GRZgIIIBgFF7/////////////////////////////////////4OBgxYXg1JQUFBQYEBRYQOMkZBhLEBWW2AAYEBRgIMDgYVa9JFQUD2AYACBFGEDx1dgQFGRUGAfGWA/PQEWggFgQFI9glI9YABgIIQBPmEDzFZbYGCRUFtQkVCRUIFhA91XYBVhA/JWW4CAYCABkFGBAZBhA/GRkGEskFZbW2ADC5JQUFCUk1BQUFBWW2AAgGAAYQFnc///////////////////////////FmMV2svqYOAbiIiIiGBAUWAkAWEEPpSTkpGQYSu/VltgQFFgIIGDAwOBUpBgQFKQe/////////////////////////////////////8ZFmAgggGAUXv/////////////////////////////////////g4GDFheDUlBQUFBgQFFhBKiRkGEsQFZbYABgQFGAgwOBYACGWvGRUFA9gGAAgRRhBOVXYEBRkVBgHxlgPz0BFoIBYEBSPYJSPWAAYCCEAT5hBOpWW2BgkVBbUJFQkVCBYQT7V2AVYQUQVluAgGAgAZBRgQGQYQUPkZBhLJBWW1tgAwuSUFBQlJNQUFBQVltgAGAFZ///////////gREVYQU8V2EFO2Ei/FZbW2BAUZCAglKAYCACYCABggFgQFKAFWEFdVeBYCABW2EFYmEgclZbgVJgIAGQYAGQA5CBYQVaV5BQW1CQUGEFh2AAYAFgAolhFshWW4FgAIFRgRBhBZtXYQWaYSy9VltbYCACYCABAYGQUlBhBbRgAmADgIhhFshWW4FgAYFRgRBhBchXYQXHYSy9VltbYCACYCABAYGQUlBhBeBgBGABhmEXAVZbgWACgVGBEGEF9FdhBfNhLL1WW1tgIAJgIAEBgZBSUGEGDGAGYAGGYRcBVluBYAOBUYEQYQYgV2EGH2EsvVZbW2AgAmAgAQGBkFJQYQY4YAVgBIZhFwFWW4FgBIFRgRBhBkxXYQZLYSy9VltbYCACYCABAYGQUlBhBl9hIJJWW2AAgWAAAZBgBwuQgWAHC4FSUFCDgWAgAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUIKBYEABkGAHC5CBYAcLgVJQUGEGxGEgz1ZbiIFgQAGQc///////////////////////////FpCBc///////////////////////////FoFSUFCCgWDgAYGQUlCBgWEBAAGBkFJQYABhBxuLg2EXOFZbkFBgFmADC4EUYQdjV2BAUX8Iw3mgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIFSYAQBYQdakGEtSVZbYEBRgJEDkP1bUFBQUFBQUFBQUFBWW2EHeGEhPlZbYACAYQeFhYVhGFBWW5FQYAcLkVBgFmADC4IUYQecV2AAgP1bgJJQUFCSkVBQVltgAIBhAWdz//////////////////////////8WY2GNxl5g4BuFhWBAUWAkAWEH3pKRkGEts1ZbYEBRYCCBgwMDgVKQYEBSkHv/////////////////////////////////////GRZgIIIBgFF7/////////////////////////////////////4OBgxYXg1JQUFBQYEBRYQhIkZBhLEBWW2AAYEBRgIMDgWAAhlrxkVBQPYBgAIEUYQiFV2BAUZFQYB8ZYD89ARaCAWBAUj2CUj1gAGAghAE+YQiKVltgYJFQW1CRUJFQf0r0eA4G/oy532SweU+m8BOZr5eRdbuYjjXg5X5ZRWe8goJgQFFhCMCSkZBhLfJWW2BAUYCRA5ChUFBQUFZbYABgBWf//////////4ERFWEI61dhCOphIvxWW1tgQFGQgIJSgGAgAmAgAYIBYEBSgBVhCSRXgWAgAVthCRFhIHJWW4FSYCABkGABkAOQgWEJCVeQUFtQkFBhCTZgAGABYAKHYRbIVluBYACBUYEQYQlKV2EJSWEsvVZbW2AgAmAgAQGBkFJQYQljYAJgA4CGYRbIVluBYAGBUYEQYQl3V2EJdmEsvVZbW2AgAmAgAQGBkFJQYQmPYARgAYRhFwFWW4FgAoFRgRBhCaNXYQmiYSy9VltbYCACYCABAYGQUlBhCbtgBmABhGEXAVZbgWADgVGBEGEJz1dhCc5hLL1WW1tgIAJgIAEBgZBSUGEJ52AFYASEYRcBVluBYASBUYEQYQn7V2EJ+mEsvVZbW2AgAmAgAQGBkFJQYABhChKGg2EZhFZbYAcLkFBgFmADC4EUYQpdV2BAUX8Iw3mgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIFSYAQBYQpUkGEublZbYEBRgJEDkP1bUFBQUFBQVltgAGABZ///////////gREVYQqCV2EKgWEi/FZbW2BAUZCAglKAYCACYCABggFgQFKAFWEKu1eBYCABW2EKqGEgclZbgVJgIAGQYAGQA5CBYQqgV5BQW1CQUGEKxmEgclZbYASBYAABgYFSUFBhCtlhIT5WWzCBYCABkHP//////////////////////////xaQgXP//////////////////////////xaBUlBQYAGBYAABkBUVkIEVFYFSUFCAgmAgAYGQUlCBg2AAgVGBEGELQFdhCz9hLL1WW1tgIAJgIAEBgZBSUGAAYQtbMGAAiIiIYRqcVluQUGAAYQtpiINhFzhWW5BQYBZgAwuBFGELe1dgAID9W1BQUFBQUFBQVltgAIBgAGEBZ3P//////////////////////////xZjmyPT2WDgG4iIiIhgQFFgJAFhC8KUk5KRkGErv1ZbYEBRYCCBgwMDgVKQYEBSkHv/////////////////////////////////////GRZgIIIBgFF7/////////////////////////////////////4OBgxYXg1JQUFBQYEBRYQwskZBhLEBWW2AAYEBRgIMDgWAAhlrxkVBQPYBgAIEUYQxpV2BAUZFQYB8ZYD89ARaCAWBAUj2CUj1gAGAghAE+YQxuVltgYJFQW1CRUJFQgWEMf1dgFWEMlFZbgIBgIAGQUYEBkGEMk5GQYSyQVltbYAMLklBQUJSTUFBQUFZbYABgBWf//////////4ERFWEMwFdhDL9hIvxWW1tgQFGQgIJSgGAgAmAgAYIBYEBSgBVhDPlXgWAgAVthDOZhIHJWW4FSYCABkGABkAOQgWEM3leQUFtQkFBhDQtgAGABYAKMYRbIVluBYACBUYEQYQ0fV2ENHmEsvVZbW2AgAmAgAQGBkFJQYQ04YAJgA4CLYRbIVluBYAGBUYEQYQ1MV2ENS2EsvVZbW2AgAmAgAQGBkFJQYQ1kYARgAYlhFwFWW4FgAoFRgRBhDXhXYQ13YSy9VltbYCACYCABAYGQUlBhDZBgBmABiWEXAVZbgWADgVGBEGENpFdhDaNhLL1WW1tgIAJgIAEBgZBSUGENvGAFYASJYRcBVluBYASBUYEQYQ3QV2ENz2EsvVZbW2AgAmAgAQGBkFJQg2ACkIFhDeqRkGEwpVZbUIJgA5CBYQ36kZBhMKVWW1CBYASQgWEOCpGQYTClVltQYABhDhuLYACJiYZhGpxWW5BQYABhDimNg2EXOFZbkFBgFmADC4EUYQ5xV2BAUX8Iw3mgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIFSYAQBYQ5okGEtSVZbYEBRgJEDkP1bUFBQUFBQUFBQUFBQUFZbYACAYABhAWdz//////////////////////////8WYxXay+pg4BuIiIiIYEBRYCQBYQ69lJOSkZBhK79WW2BAUWAggYMDA4FSkGBAUpB7/////////////////////////////////////xkWYCCCAYBRe/////////////////////////////////////+DgYMWF4NSUFBQUGBAUWEPJ5GQYSxAVltgAGBAUYCDA4GFWvSRUFA9gGAAgRRhD2JXYEBRkVBgHxlgPz0BFoIBYEBSPYJSPWAAYCCEAT5hD2dWW2BgkVBbUJFQkVCBYQ94V2AVYQ+NVluAgGAgAZBRgQGQYQ+MkZBhLJBWW1tgAwuSUFBQlJNQUFBQVltgAGAFZ///////////gREVYQ+5V2EPuGEi/FZbW2BAUZCAglKAYCACYCABggFgQFKAFWEP8leBYCABW2EP32EgclZbgVJgIAGQYAGQA5CBYQ/XV5BQW1CQUGEQBGAAYAFgAodhFshWW4FgAIFRgRBhEBhXYRAXYSy9VltbYCACYCABAYGQUlBhEDFgAmADgIZhFshWW4FgAYFRgRBhEEVXYRBEYSy9VltbYCACYCABAYGQUlBhEF1gBGABhGEXAVZbgWACgVGBEGEQcVdhEHBhLL1WW1tgIAJgIAEBgZBSUGEQiWAGYAGEYRcBVluBYAOBUYEQYRCdV2EQnGEsvVZbW2AgAmAgAQGBkFJQYRC1YAVgBIRhFwFWW4FgBIFRgRBhEMlXYRDIYSy9VltbYCACYCABAYGQUlBhENxhIM9WW4WBYEABkHP//////////////////////////xaQgXP//////////////////////////xaBUlBQYAKAVGERIZBhLshWW4BgHwFgIICRBAJgIAFgQFGQgQFgQFKAkpGQgYFSYCABgoBUYRFNkGEuyFZbgBVhEZpXgGAfEGERb1dhAQCAg1QEAoNSkWAgAZFhEZpWW4IBkZBgAFJgIGAAIJBbgVSBUpBgAQGQYCABgIMRYRF9V4KQA2AfFoIBkVtQUFBQUIFgAAGBkFJQYAOAVGERtJBhLshWW4BgHwFgIICRBAJgIAFgQFGQgQFgQFKAkpGQgYFSYCABgoBUYRHgkGEuyFZbgBVhEi1XgGAfEGESAldhAQCAg1QEAoNSkWAgAZFhEi1WW4IBkZBgAFJgIGAAIJBbgVSBUpBgAQGQYCABgIMRYRIQV4KQA2AfFoIBkVtQUFBQUIFgIAGBkFJQgYFg4AGBkFJQYASAVGESUJBhLshWW4BgHwFgIICRBAJgIAFgQFGQgQFgQFKAkpGQgYFSYCABgoBUYRJ8kGEuyFZbgBVhEslXgGAfEGESnldhAQCAg1QEAoNSkWAgAZFhEslWW4IBkZBgAFJgIGAAIJBbgVSBUpBgAQGQYCABgIMRYRKsV4KQA2AfFoIBkVtQUFBQUIFgYAGBkFJQYABhEuKIg2EXOFZbkFBgFmADC4EUYRMqV2BAUX8Iw3mgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIFSYAQBYRMhkGExw1ZbYEBRgJEDkP1bUFBQUFBQUFBWW2ETPGEgz1ZbYAKAVGETSZBhLshWW4BgHwFgIICRBAJgIAFgQFGQgQFgQFKAkpGQgYFSYCABgoBUYRN1kGEuyFZbgBVhE8JXgGAfEGETl1dhAQCAg1QEAoNSkWAgAZFhE8JWW4IBkZBgAFJgIGAAIJBbgVSBUpBgAQGQYCABgIMRYROlV4KQA2AfFoIBkVtQUFBQUIFgAAGBkFJQYAOAVGET3JBhLshWW4BgHwFgIICRBAJgIAFgQFGQgQFgQFKAkpGQgYFSYCABgoBUYRQIkGEuyFZbgBVhFFVXgGAfEGEUKldhAQCAg1QEAoNSkWAgAZFhFFVWW4IBkZBgAFJgIGAAIJBbgVSBUpBgAQGQYCABgIMRYRQ4V4KQA2AfFoIBkVtQUFBQUIFgIAGBkFJQgYFgQAGQc///////////////////////////FpCBc///////////////////////////FoFSUFBgBIBUYRSnkGEuyFZbgGAfAWAggJEEAmAgAWBAUZCBAWBAUoCSkZCBgVJgIAGCgFRhFNOQYS7IVluAFWEVIFeAYB8QYRT1V2EBAICDVAQCg1KRYCABkWEVIFZbggGRkGAAUmAgYAAgkFuBVIFSkGABAZBgIAGAgxFhFQNXgpADYB8WggGRW1BQUFBQgWBgAYGQUlBgAGEVOYSDYRc4VluQUGAWYAMLgRRhFYFXYEBRfwjDeaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgVJgBAFhFXiQYTJVVltgQFGAkQOQ/VtQUFBQVlthFY9hIM9WW4KBYAABgZBSUIGBYCABgZBSUIOBYEABkHP//////////////////////////xaQgXP//////////////////////////xaBUlBQYASAVGEV5pBhLshWW4BgHwFgIICRBAJgIAFgQFGQgQFgQFKAkpGQgYFSYCABgoBUYRYSkGEuyFZbgBVhFl9XgGAfEGEWNFdhAQCAg1QEAoNSkWAgAZFhFl9WW4IBkZBgAFJgIGAAIJBbgVSBUpBgAQGQYCABgIMRYRZCV4KQA2AfFoIBkVtQUFBQUIFgYAGBkFJQYABhFniGg2EXOFZbkFBgFmADC4EUYRbAV2BAUX8Iw3mgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIFSYAQBYRa3kGEy51ZbYEBRgJEDkP1bUFBQUFBQVlthFtBhIHJWW2BAUYBgQAFgQFKAYRblh4dhHRZWW4FSYCABYRb0hYVhHWxWW4FSUJBQlJNQUFBQVlthFwlhIHJWW2BAUYBgQAFgQFKAYRcdhmEfQVZbgVJgIAFhFyyFhWEfglZbgVJQkFCTklBQUFZbYACAYABhAWdz//////////////////////////8WY30wXPpg4BuGhmBAUWAkAWEXcZKRkGE2B1ZbYEBRYCCBgwMDgVKQYEBSkHv/////////////////////////////////////GRZgIIIBgFF7/////////////////////////////////////4OBgxYXg1JQUFBQYEBRYRfbkZBhLEBWW2AAYEBRgIMDgWAAhlrxkVBQPYBgAIEUYRgYV2BAUZFQYB8ZYD89ARaCAWBAUj2CUj1gAGAghAE+YRgdVltgYJFQW1CRUJFQgWEYLldgFWEYQ1ZbgIBgIAGQUYEBkGEYQpGQYSyQVltbYAMLklBQUJKRUFBWW2AAYRhaYSE+VltgAIBhAWdz//////////////////////////8WYzxN0y5g4BuHh2BAUWAkAWEYkZKRkGE2N1ZbYEBRYCCBgwMDgVKQYEBSkHv/////////////////////////////////////GRZgIIIBgFF7/////////////////////////////////////4OBgxYXg1JQUFBQYEBRYRj7kZBhLEBWW2AAYEBRgIMDgWAAhlrxkVBQPYBgAIEUYRk4V2BAUZFQYB8ZYD89ARaCAWBAUj2CUj1gAGAghAE+YRk9VltgYJFQW1CRUJFQYRlKYSE+VluCYRlXV2AVgWEZbFZbgYBgIAGQUYEBkGEZa5GQYTffVltbgWADC5FQgJVQgZZQUFBQUFCSUJKQUFZbYACAYABhAWdz//////////////////////////8WY2/Dy69g4BuGhmBAUWAkAWEZvZKRkGE4wVZbYEBRYCCBgwMDgVKQYEBSkHv/////////////////////////////////////GRZgIIIBgFF7/////////////////////////////////////4OBgxYXg1JQUFBQYEBRYRonkZBhLEBWW2AAYEBRgIMDgWAAhlrxkVBQPYBgAIEUYRpkV2BAUZFQYB8ZYD89ARaCAWBAUj2CUj1gAGAghAE+YRppVltgYJFQW1CRUJFQgWEaeldgFWEaj1ZbgIBgIAGQUYEBkGEajpGQYSyQVltbYAMLklBQUJKRUFBWW2EapGEgz1ZbYRqsYSCSVluFgWAAAZBgBwuQgWAHC4FSUFCEgWAgAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUIOBYEABkGAHC5CBYAcLgVJQUGACgFRhGxWQYS7IVluAYB8BYCCAkQQCYCABYEBRkIEBYEBSgJKRkIGBUmAgAYKAVGEbQZBhLshWW4AVYRuOV4BgHxBhG2NXYQEAgINUBAKDUpFgIAGRYRuOVluCAZGQYABSYCBgACCQW4FUgVKQYAEBkGAgAYCDEWEbcVeCkANgHxaCAZFbUFBQUFCCYAABgZBSUGADgFRhG6iQYS7IVluAYB8BYCCAkQQCYCABYEBRkIEBYEBSgJKRkIGBUmAgAYKAVGEb1JBhLshWW4AVYRwhV4BgHxBhG/ZXYQEAgINUBAKDUpFgIAGRYRwhVluCAZGQYABSYCBgACCQW4FUgVKQYAEBkGAgAYCDEWEcBFeCkANgHxaCAZFbUFBQUFCCYCABgZBSUIaCYEABkHP//////////////////////////xaQgXP//////////////////////////xaBUlBQgoJg4AGBkFJQgIJhAQABgZBSUGAEgFRhHIaQYS7IVluAYB8BYCCAkQQCYCABYEBRkIEBYEBSgJKRkIGBUmAgAYKAVGEcspBhLshWW4AVYRz/V4BgHxBhHNRXYQEAgINUBAKDUpFgIAGRYRz/VluCAZGQYABSYCBgACCQW4FUgVKQYAEBkGAgAYCDEWEc4leCkANgHxaCAZFbUFBQUFCCYGABgZBSUFCVlFBQUFBQVltgAGEdPYNgBoERFWEdLldhHS1hOPFWW1uCYSBekJGQY/////8WVluQUGEdZIJgBoERFWEdVVdhHVRhOPFWW1uCYSBekJGQY/////8WVluQUJKRUFBWW2EddGEhPlZbYABgBIERFWEdiFdhHYdhOPFWW1uDYASBERVhHZtXYR2aYTjxVltbA2EdtldgAYFgAAGQFRWQgRUVgVJQUGEfO1ZbYAFgBIERFWEdyldhHclhOPFWW1uDYASBERVhHd1XYR3cYTjxVltbA2EeP1dgAIBUkGEBAAqQBHP//////////////////////////xaBYCABkHP//////////////////////////xaQgXP//////////////////////////xaBUlBQYR86VltgAmAEgREVYR5TV2EeUmE48VZbW4NgBIERFWEeZldhHmVhOPFWW1sDYR55V4GBYEABgZBSUGEfOVZbYANgBIERFWEejVdhHoxhOPFWW1uDYASBERVhHqBXYR6fYTjxVltbA2Ees1eBgWBgAYGQUlBhHzhWW2AEgIERFWEexldhHsVhOPFWW1uDYASBERVhHtlXYR7YYTjxVltbA2EfN1dgAIBUkGEBAAqQBHP//////////////////////////xaBYIABkHP//////////////////////////xaQgXP//////////////////////////xaBUlBQW1tbW1uSkVBQVltgAGABYACDYAaBERVhH1pXYR9ZYTjxVltbYAaBERVhH2xXYR9rYTjxVltbgVJgIAGQgVJgIAFgACBUkFCRkFBWW2EfimEhPlZbYAFgBIERFWEfnldhH51hOPFWW1uDYASBERVhH7FXYR+wYTjxVltbA2Ef81eBgWAgAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUGEgWFZbYASAgREVYSAGV2EgBWE48VZbW4NgBIERFWEgGVdhIBhhOPFWW1sDYSBXV4GBYIABkHP//////////////////////////xaQgXP//////////////////////////xaBUlBQW1uSkVBQVltgAIFg/xZgAZAbgxeQUJKRUFBWW2BAUYBgQAFgQFKAYACBUmAgAWEgjGEhPlZbgVJQkFZbYEBRgGBgAWBAUoBgAGAHC4FSYCABYABz//////////////////////////8WgVJgIAFgAGAHC4FSUJBWW2BAUYBhASABYEBSgGBggVJgIAFgYIFSYCABYABz//////////////////////////8WgVJgIAFgYIFSYCABYAAVFYFSYCABYABgBwuBUmAgAWAAFRWBUmAgAWBggVJgIAFhIThhIJJWW4FSUJBWW2BAUYBgoAFgQFKAYAAVFYFSYCABYABz//////////////////////////8WgVJgIAFgYIFSYCABYGCBUmAgAWAAc///////////////////////////FoFSUJBWW2AAYEBRkFCQVltgAID9W2AAgP1bYABz//////////////////////////+CFpBQkZBQVltgAGEh2oJhIa9WW5BQkZBQVlthIeqBYSHPVluBFGEh9VdgAID9W1BWW2AAgTWQUGEiB4FhIeFWW5KRUFBWW2AAgZBQkZBQVlthIiCBYSINVluBFGEiK1dgAID9W1BWW2AAgTWQUGEiPYFhIhdWW5KRUFBWW2AAgGAAgGCAhYcDEhVhIl1XYSJcYSGlVltbYABhImuHgogBYSH4VluUUFBgIGEifIeCiAFhIfhWW5NQUGBAYSKNh4KIAWEh+FZbklBQYGBhIp6HgogBYSIuVluRUFCSlZGUUJJQVltgAIFgBwuQUJGQUFZbYSLAgWEiqlZbglJQUFZbYABgIIIBkFBhIttgAIMBhGEit1ZbkpFQUFZbYACA/VtgAID9W2AAYB8ZYB+DARaQUJGQUFZbf05Ie3EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYABSYEFgBFJgJGAA/VthIzSCYSLrVluBAYGBEGf//////////4IRFxVhI1NXYSNSYSL8VltbgGBAUlBQUFZbYABhI2ZhIZtWW5BQYSNygoJhIytWW5GQUFZbYABn//////////+CERVhI5JXYSORYSL8VltbYSObgmEi61ZbkFBgIIEBkFCRkFBWW4KBgzdgAIODAVJQUFBWW2AAYSPKYSPFhGEjd1ZbYSNcVluQUIKBUmAggQGEhIQBERVhI+ZXYSPlYSLmVltbYSPxhIKFYSOoVltQk5JQUFBWW2AAgmAfgwESYSQOV2EkDWEi4VZbW4E1YSQehIJgIIYBYSO3VluRUFCSkVBQVlthJDCBYSKqVluBFGEkO1dgAID9W1BWW2AAgTWQUGEkTYFhJCdWW5KRUFBWW2AAgGAAgGAAgGAAYOCIigMSFWEkcldhJHFhIaVWW1tgAGEkgIqCiwFhIfhWW5dQUGAgYSSRioKLAWEh+FZbllBQYECIATVn//////////+BERVhJLJXYSSxYSGqVltbYSS+ioKLAWEj+VZblVBQYGCIATVn//////////+BERVhJN9XYSTeYSGqVltbYSTrioKLAWEj+VZblFBQYIBhJPyKgosBYSH4VluTUFBgoGElDYqCiwFhIfhWW5JQUGDAYSUeioKLAWEkPlZbkVBQkpWYkZSXUJKVUFZbYACAYECDhQMSFWElRFdhJUNhIaVWW1tgAGElUoWChgFhIfhWW5JQUGAgYSVjhYKGAWEiLlZbkVBQklCSkFBWW2AAgRUVkFCRkFBWW2ElgoFhJW1WW4JSUFBWW2ElkYFhIc9WW4JSUFBWW2AAgVGQUJGQUFZbYACCglJgIIIBkFCSkVBQVltgAFuDgRAVYSXRV4CCAVGBhAFSYCCBAZBQYSW2VltgAISEAVJQUFBQVltgAGEl6IJhJZdWW2El8oGFYSWiVluTUGEmAoGFYCCGAWEls1ZbYSYLgWEi61ZbhAGRUFCSkVBQVltgAGCggwFgAIMBUWEmLmAAhgGCYSV5VltQYCCDAVFhJkFgIIYBgmEliFZbUGBAgwFRhIIDYECGAVJhJlmCgmEl3VZbkVBQYGCDAVGEggNgYIYBUmEmc4KCYSXdVluRUFBggIMBUWEmiGCAhgGCYSWIVltQgJFQUJKRUFBWW2AAYCCCAZBQgYEDYACDAVJhJq2BhGEmFlZbkFCSkVBQVltgAIBgQIOFAxIVYSbMV2Emy2EhpVZbW2AAYSbahYKGAWEh+FZbklBQYCCDATVn//////////+BERVhJvtXYSb6YSGqVltbYScHhYKGAWEj+VZbkVBQklCSkFBWW2AAgGAAgGCAhYcDEhVhJytXYScqYSGlVltbYABhJzmHgogBYSH4VluUUFBgIIUBNWf//////////4ERFWEnWldhJ1lhIapWW1thJ2aHgogBYSP5VluTUFBgQIUBNWf//////////4ERFWEnh1dhJ4ZhIapWW1thJ5OHgogBYSP5VluSUFBgYGEnpIeCiAFhIfhWW5FQUJKVkZRQklBWW2AAgGAAYGCEhgMSFWEnyVdhJ8hhIaVWW1tgAGEn14aChwFhIfhWW5NQUGAgYSfohoKHAWEh+FZbklBQYEBhJ/mGgocBYSQ+VluRUFCSUJJQklZbYABn//////////+CERVhKB5XYSgdYSL8VltbYSgngmEi61ZbkFBgIIEBkFCRkFBWW2AAYShHYShChGEoA1ZbYSNcVluQUIKBUmAggQGEhIQBERVhKGNXYShiYSLmVltbYShuhIKFYSOoVltQk5JQUFBWW2AAgmAfgwESYSiLV2EoimEi4VZbW4E1YSibhIJgIIYBYSg0VluRUFCSkVBQVltgAIBgAIBgAIBgAIBgAIBhAUCLjQMSFWEoyFdhKMdhIaVWW1tgAGEo1o2CjgFhIfhWW5pQUGAgYSjnjYKOAWEh+FZbmVBQYECLATVn//////////+BERVhKQhXYSkHYSGqVltbYSkUjYKOAWEj+VZbmFBQYGCLATVn//////////+BERVhKTVXYSk0YSGqVltbYSlBjYKOAWEj+VZbl1BQYIBhKVKNgo4BYSH4VluWUFBgoGEpY42CjgFhIfhWW5VQUGDAYSl0jYKOAWEkPlZblFBQYOCLATVn//////////+BERVhKZVXYSmUYSGqVltbYSmhjYKOAWEodlZbk1BQYQEAiwE1Z///////////gREVYSnDV2EpwmEhqlZbW2Epz42CjgFhKHZWW5JQUGEBIIsBNWf//////////4ERFWEp8VdhKfBhIapWW1thKf2Ngo4BYSh2VluRUFCSlZibkZSXmlCSlZhQVltgAIBgAIBgAGCghogDEhVhKitXYSoqYSGlVltbYABhKjmIgokBYSH4VluVUFBgIGEqSoiCiQFhIfhWW5RQUGBAhgE1Z///////////gREVYSprV2EqamEhqlZbW2Eqd4iCiQFhI/lWW5NQUGBghgE1Z///////////gREVYSqYV2Eql2EhqlZbW2EqpIiCiQFhI/lWW5JQUGCAYSq1iIKJAWEh+FZbkVBQkpVQkpWQk1BWW2AAgGBAg4UDEhVhKtlXYSrYYSGlVltbYABhKueFgoYBYSH4VluSUFBgIGEq+IWChgFhIfhWW5FQUJJQkpBQVltgAIBgAIBggIWHAxIVYSscV2ErG2EhpVZbW2AAYSsqh4KIAWEh+FZblFBQYCBhKzuHgogBYSH4VluTUFBgQIUBNWf//////////4ERFWErXFdhK1thIapWW1thK2iHgogBYSh2VluSUFBgYIUBNWf//////////4ERFWEriVdhK4hhIapWW1thK5WHgogBYSh2VluRUFCSlZGUUJJQVlthK6qBYSHPVluCUlBQVlthK7mBYSINVluCUlBQVltgAGCAggGQUGEr1GAAgwGHYSuhVlthK+FgIIMBhmEroVZbYSvuYECDAYVhK6FWW2Er+2BggwGEYSuwVluVlFBQUFBQVltgAIGQUJKRUFBWW2AAYSwagmEll1ZbYSwkgYVhLARWW5NQYSw0gYVgIIYBYSWzVluAhAGRUFCSkVBQVltgAGEsTIKEYSwPVluRUIGQUJKRUFBWW2AAgWADC5BQkZBQVlthLG2BYSxXVluBFGEseFdgAID9W1BWW2AAgVGQUGEsioFhLGRWW5KRUFBWW2AAYCCChAMSFWEspldhLKVhIaVWW1tgAGEstISChQFhLHtWW5FQUJKRUFBWW39OSHtxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAUmAyYARSYCRgAP1bYACCglJgIIIBkFCSkVBQVlt/VXBkYXRlIG9mIHRva2VuSW5mbyBmYWlsZWQhAAAAAABgAIIBUlBWW2AAYS0zYBuDYSzsVluRUGEtPoJhLP1WW2AgggGQUJGQUFZbYABgIIIBkFCBgQNgAIMBUmEtYoFhLSZWW5BQkZBQVltgAIKCUmAgggGQUJKRUFBWW2AAYS2FgmEll1ZbYS2PgYVhLWlWW5NQYS2fgYVgIIYBYSWzVlthLaiBYSLrVluEAZFQUJKRUFBWW2AAYECCAZBQYS3IYACDAYVhK6FWW4GBA2AggwFSYS3agYRhLXpWW5BQk5JQUFBWW2Et7IFhJW1WW4JSUFBWW2AAYECCAZBQYS4HYACDAYVhLeNWW4GBA2AggwFSYS4ZgYRhLXpWW5BQk5JQUFBWW39VcGRhdGUgb2YgdG9rZW4ga2V5cyBmYWlsZWQhAAAAAGAAggFSUFZbYABhLlhgHINhLOxWW5FQYS5jgmEuIlZbYCCCAZBQkZBQVltgAGAgggGQUIGBA2AAgwFSYS6HgWEuS1ZbkFCRkFBWW2AAgVGQUJGQUFZbf05Ie3EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYABSYCJgBFJgJGAA/VtgAGACggSQUGABghaAYS7gV2B/ghaRUFtgIIIQgQNhLvNXYS7yYS6ZVltbUJGQUFZbYACBkFCBYABSYCBgACCQUJGQUFZbYABgIGAfgwEEkFCRkFBWW2AAgoIbkFCSkVBQVltgAGAIgwJhL1t///////////////////////////////////////////+CYS8eVlthL2WGg2EvHlZblVCAGYQWk1CAhhaEF5JQUFCTklBQUFZbYACBkFCRkFBWW2AAYS+iYS+dYS+YhGEiDVZbYS99VlthIg1WW5BQkZBQVltgAIGQUJGQUFZbYS+8g2Evh1ZbYS/QYS/IgmEvqVZbhIRUYS8rVluCVVBQUFBWW2AAkFZbYS/lYS/YVlthL/CBhIRhL7NWW1BQUFZbW4GBEBVhMBRXYTAJYACCYS/dVltgAYEBkFBhL/ZWW1BQVltgH4IRFWEwWVdhMCqBYS75VlthMDOEYS8OVluBAWAghRAVYTBCV4GQUFthMFZhME6FYS8OVluDAYJhL/VWW1BQW1BQUFZbYACCghyQUJKRUFBWW2AAYTB8YAAZhGAIAmEwXlZbGYCDFpFQUJKRUFBWW2AAYTCVg4NhMGtWW5FQgmACAoIXkFCSkVBQVlthMK6CYS6OVltn//////////+BERVhMMdXYTDGYSL8VltbYTDRglRhLshWW2Ew3IKChWEwGFZbYABgIJBQYB+DEWABgRRhMQ9XYACEFWEw/VeChwFRkFBbYTEHhYJhMIlWW4ZVUGExb1ZbYB8ZhBZhMR2GYS75VltgAFuCgRAVYTFFV4SJAVGCVWABggGRUGAghQGUUGAggQGQUGExIFZbhoMQFWExYleEiQFRYTFeYB+JFoJhMGtWW4NVUFtgAWACiAIBiFVQUFBbUFBQUFBQVlt/VXBkYXRlIG9mIHRva2VuSW5mbyBrZXlzIGZhaWxlZCFgAIIBUlBWW2AAYTGtYCCDYSzsVluRUGExuIJhMXdWW2AgggGQUJGQUFZbYABgIIIBkFCBgQNgAIMBUmEx3IFhMaBWW5BQkZBQVlt/VXBkYXRlIG9mIHRva2VuSW5mby50cmVhc3VyeSBmYWlgAIIBUn9sZWQhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAgggFSUFZbYABhMj9gJINhLOxWW5FQYTJKgmEx41ZbYECCAZBQkZBQVltgAGAgggGQUIGBA2AAgwFSYTJugWEyMlZbkFCRkFBWW39VcGRhdGUgb2YgdG9rZW5JbmZvIG5hbWUgYW5kIHN5bWAAggFSf2JvbCBmYWlsZWQhAAAAAAAAAAAAAAAAAAAAAAAAAAAAYCCCAVJQVltgAGEy0WArg2Es7FZbkVBhMtyCYTJ1VltgQIIBkFCRkFBWW2AAYCCCAZBQgYEDYACDAVJhMwCBYTLEVluQUJGQUFZbYACCglJgIIIBkFCSkVBQVltgAGEzI4JhLo5WW2EzLYGFYTMHVluTUGEzPYGFYCCGAWEls1ZbYTNGgWEi61ZbhAGRUFCSkVBQVlthM1qBYSKqVluCUlBQVltgAIFRkFCRkFBWW2AAgoJSYCCCAZBQkpFQUFZbYACBkFBgIIIBkFCRkFBWW2EzlYFhIg1WW4JSUFBWW2AAYKCDAWAAgwFRYTOzYACGAYJhJXlWW1BgIIMBUWEzxmAghgGCYSWIVltQYECDAVGEggNgQIYBUmEz3oKCYSXdVluRUFBgYIMBUYSCA2BghgFSYTP4goJhJd1WW5FQUGCAgwFRYTQNYICGAYJhJYhWW1CAkVBQkpFQUFZbYABgQIMBYACDAVFhNDBgAIYBgmEzjFZbUGAggwFRhIIDYCCGAVJhNEiCgmEzm1ZbkVBQgJFQUJKRUFBWW2AAYTRhg4NhNBhWW5BQkpFQUFZbYABgIIIBkFCRkFBWW2AAYTSBgmEzYFZbYTSLgYVhM2tWW5NQg2AgggKFAWE0nYVhM3xWW4BgAFuFgRAVYTTZV4SEA4lSgVFhNLqFgmE0VVZblFBhNMWDYTRpVluSUGAgigGZUFBgAYEBkFBhNKFWW1CCl1CHlVBQUFBQUJKRUFBWW2BgggFgAIIBUWE1AWAAhQGCYTNRVltQYCCCAVFhNRRgIIUBgmEliFZbUGBAggFRYTUnYECFAYJhM1FWW1BQUFBWW2AAYQFggwFgAIMBUYSCA2AAhgFSYTVLgoJhMxhWW5FQUGAggwFRhIIDYCCGAVJhNWWCgmEzGFZbkVBQYECDAVFhNXpgQIYBgmEliFZbUGBggwFRhIIDYGCGAVJhNZKCgmEzGFZbkVBQYICDAVFhNadggIYBgmEleVZbUGCggwFRYTW6YKCGAYJhM1FWW1BgwIMBUWE1zWDAhgGCYSV5VltQYOCDAVGEggNg4IYBUmE15YKCYTR2VluRUFBhAQCDAVFhNfxhAQCGAYJhNOtWW1CAkVBQkpFQUFZbYABgQIIBkFBhNhxgAIMBhWEroVZbgYEDYCCDAVJhNi6BhGE1LVZbkFCTklBQUFZbYABgQIIBkFBhNkxgAIMBhWEroVZbYTZZYCCDAYRhK7BWW5OSUFBQVltgAID9W2AAgP1bYTZzgWElbVZbgRRhNn5XYACA/VtQVltgAIFRkFBhNpCBYTZqVluSkVBQVltgAIFRkFBhNqWBYSHhVluSkVBQVltgAGE2vmE2uYRhI3dWW2EjXFZbkFCCgVJgIIEBhISEAREVYTbaV2E22WEi5lZbW2E25YSChWEls1ZbUJOSUFBQVltgAIJgH4MBEmE3AldhNwFhIuFWW1uBUWE3EoSCYCCGAWE2q1ZbkVBQkpFQUFZbYABgoIKEAxIVYTcxV2E3MGE2YFZbW2E3O2CgYSNcVluQUGAAYTdLhIKFAWE2gVZbYACDAVJQYCBhN1+EgoUBYTaWVltgIIMBUlBgQIIBUWf//////////4ERFWE3g1dhN4JhNmVWW1thN4+EgoUBYTbtVltgQIMBUlBgYIIBUWf//////////4ERFWE3s1dhN7JhNmVWW1thN7+EgoUBYTbtVltgYIMBUlBggGE304SChQFhNpZWW2CAgwFSUJKRUFBWW2AAgGBAg4UDEhVhN/ZXYTf1YSGlVltbYABhOASFgoYBYSx7VluSUFBgIIMBUWf//////////4ERFWE4JVdhOCRhIapWW1thODGFgoYBYTcbVluRUFCSUJKQUFZbYACCglJgIIIBkFCSkVBQVltgAGE4V4JhM2BWW2E4YYGFYTg7VluTUINgIIIChQFhOHOFYTN8VluAYABbhYEQFWE4r1eEhAOJUoFRYTiQhYJhNFVWW5RQYTibg2E0aVZbklBgIIoBmVBQYAGBAZBQYTh3VltQgpdQh5VQUFBQUFCSkVBQVltgAGBAggGQUGE41mAAgwGFYSuhVluBgQNgIIMBUmE46IGEYThMVluQUJOSUFBQVlt/Tkh7cQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAFJgIWAEUmAkYAD9/qJkaXBmc1giEiDtAXAL5ZIW7AMWW4oh/ZXMz5R2xWuTq1mXhyqN/ey6ZGRzb2xjQwAIEgAzIoACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACiAqMMBOgMY7gdKFgoUAAAAAAAAAAAAAAAAAAAAAAAAA+5yBwoDGO4HEAFSGAoKCgIYAhD/r9bYAQoKCgIYYhCAsNbYAQ=="},{"b64Body":"Cg8KCQi54J6tBhCDBxICGAISAhgDGNyRtfsFIgIIeDIgw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo7qAaYCCgZUb2tlbkQSCEtCSENRS1ZSIOgHKgMY6gcyIhIgS3y90+u7ANCUdizOEKRvqGkCsDElSJ5RLUTpM/OKhS06IhIgS3y90+u7ANCUdizOEKRvqGkCsDElSJ5RLUTpM/OKhS1CIhIgS3y90+u7ANCUdizOEKRvqGkCsDElSJ5RLUTpM/OKhS1KIhIgS3y90+u7ANCUdizOEKRvqGkCsDElSJ5RLUTpM/OKhS1SIhIgS3y90+u7ANCUdizOEKRvqGkCsDElSJ5RLUTpM/OKhS1qDAj1rvmwBhDwh5KiAqIBIhIgS3y90+u7ANCUdizOEKRvqGkCsDElSJ5RLUTpM/OKhS2yASISIEt8vdPruwDQlHYszhCkb6hpArAxJUieUS1E6TPzioUt","b64Record":"CiUIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkD1IDGO8HEjAXCEWSbPlA3wEL/WJD+DJQqwc3SIJkyqqwDypYBD+A6q8KEgxcpFtXg3o2o0wgPxkaDAj14J6tBhDbyrnGAiIPCgkIueCerQYQgwcSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIAWg8KAxjvBxIICgMY6gcQ0A9yCgoDGO8HEgMY6gc="},{"b64Body":"Cg8KCQi64J6tBhCJBxICGAISAhgDGOC2iyAiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjsICCgoDGOsHEgMY7wc=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwYO+Ro7PFHxDvAfW0IJDZuP666bHz31k4+ZY5t6IeP/OAP9RGGZWxn48U9tv+F8ZGGgsI9uCerQYQg9GbayIPCgkIuuCerQYQiQcSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIA"},{"b64Body":"Cg8KCQi64J6tBhCLBxICGAISAhgDGKX7UiICCHgyIMODwq7DgsK3w4PCuXRGOMOCwq5Kw4PCi8ODwpDDg8KOigIKCgMY7wcSAxjrBw==","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIw+zxjN+ZiFfbYwN9LLILx5CN9qXeT++SqIPVbTJ4mc2jK95OIY32gUjc++kiES/giGgwI9uCerQYQq+Xq7AIiDwoJCLrgnq0GEIsHEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQi74J6tBhCNBxICGAISAhgDGOufNiICCHgyIMODwq7DgsK3w4PCuXRGOMOCwq5Kw4PCi8ODwpDDg8KOchsSGQoDGO8HEggKAxjqBxDnBxIICgMY6wcQ6Ac=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwgmleara0l0L3rVbIayQmQLqySFQyHeQLrSzkEPQ7orS5wkYXb9PdKHMYWF4evBhNGgsI9+CerQYQ69j9dCIPCgkIu+CerQYQjQcSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIAWhkKAxjvBxIICgMY6gcQ5wcSCAoDGOsHEOgH"},{"b64Body":"ChAKCQi74J6tBhCPBxIDGOsHEgIYAxiA/rWHASICCHgyIMODwq7DgsK3w4PCuXRGOMOCwq5Kw4PCi8ODwpDDg8KOOnYKAxjuBxCAkvQBGICo1rkHImR9y7xyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD6wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAehIA","b64Record":"CiUIISIDGO4HKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjAOZ7NUKXYHXI4Xd2vgiJ09ICMYOZMDEjcnupWqoWrR2yUHcbbA6UbCO0ZDpj8NH4AaDAj34J6tBhDrw+D6AiIQCgkIu+CerQYQjwcSAxjrByogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo4wtt6qhQE6CRoCMHgomrDwAVIZCgoKAhhiEOy81YoCCgsKAxjrBxDrvNWKAg=="}]},"updateNftTokenKeysWithWrongTokenIdAndMissingAdminKey":{"placeholderNum":1008,"encodedItems":[{"b64Body":"Cg8KCQjA4J6tBhCnBxICGAISAhgDGPuV9hQiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjloxCiISIAfk5r6KbkrQsSXlKkhg3McszRMlq++exh/Aa9vbEyvDEICU69wDSgUIgM7aAw==","b64Record":"CiUIFhIDGPEHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjB6V9XdcXbYSogAjfXb1BuSw6trUsUtdPhLPMT8TN/qp5fsaO4XYYV9E/wmCwsBnYoaDAj84J6tBhCr9sOIAiIPCgkIwOCerQYQpwcSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIZCgoKAhgCEP+n1rkHCgsKAxjxBxCAqNa5Bw=="},{"b64Body":"Cg8KCQjB4J6tBhCpBxICGAISAhgDGPuV9hQiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlozCiISIGR8v9b6YBgq0K4Bi5BUcKtCTQuzdLkW4qH3YdWhEfWfEICA6YOx3hZKBQiAztoD","b64Record":"CiUIFhIDGPIHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjDD9wkVZFZJ5oQk6t30QQilBGndJ0xIhPYbMRTH0P/BZ1zuJWITvOLBiovCxc2bMGcaCwj94J6tBhDD8I8tIg8KCQjB4J6tBhCpBxICGAIqIMODwq7DgsK3w4PCuXRGOMOCwq5Kw4PCi8ODwpDDg8KOUh0KDAoCGAIQ///Rh+K8LQoNCgMY8gcQgIDSh+K8LQ=="},{"b64Body":"Cg8KCQjB4J6tBhCrBxICGAISAhgDGPuV9hQiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjloxCiISICnADX3PCNp/yAHxxe/7FGMe4sFWBMscmvoG7+FPiTPoEICU69wDSgUIgM7aAw==","b64Record":"CiUIFhIDGPMHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjD0aHQ3N8LCjkfO2v/zPt1HAemOWL757IV0B+AalyOYptrG9waFgnKTTcaONABndvMaDAj94J6tBhCThKSSAiIPCgkIweCerQYQqwcSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIZCgoKAhgCEP+n1rkHCgsKAxjzBxCAqNa5Bw=="},{"b64Body":"Cg8KCQjC4J6tBhCtBxICGAISAhgDGP7LzSwiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjooBjgESCwj+rvmwBhCI3LIhGm0KIhIgks54S+yXF7zsHmg9EzMh3CBNwYCWcNhIpn0h1mKshX0KIzohAgTLqdX1kqFBhZoO6TTHhus43rJPiPzTlMpTdoYptuNBCiISIAZiauaF+APJJFSQmwKT4MBf9x2KwOgcrSUtRIPWYCOQIgxIZWxsbyBXb3JsZCEqADIA","b64Record":"CiUIFhoDGPQHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjDThjY7QUuI79tos2Awfd/RN0to8URZWwCSlcarW/kBvAfYw89cmRCNp5exdSdEhXIaCwj+4J6tBhCLu+w7Ig8KCQjC4J6tBhCtBxICGAIqIMODwq7DgsK3w4PCuXRGOMOCwq5Kw4PCi8ODwpDDg8KOUgA="},{"b64Body":"Cg8KCQjC4J6tBhCxBxICGAISAhgDGIydjj4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjpoBiCAKAxj0ByKAIDYwODA2MDQwNTI2MDQwNTE4MDYwNDAwMTYwNDA1MjgwNjAwOTgxNTI2MDIwMDE3Zjc0NmY2YjY1NmU0ZTYxNmQ2NTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA4MTUyNTA2MDAyOTA4MTYyMDAwMDRhOTE5MDYyMDAwNTQwNTY1YjUwNjA0MDUxODA2MDQwMDE2MDQwNTI4MDYwMGI4MTUyNjAyMDAxN2Y3NDZmNmI2NTZlNTM3OTZkNjI2ZjZjMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwODE1MjUwNjAwMzkwODE2MjAwMDA5MTkxOTA2MjAwMDU0MDU2NWI1MDYwNDA1MTgwNjA0MDAxNjA0MDUyODA2MDA0ODE1MjYwMjAwMTdmNmQ2NTZkNmYwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgxNTI1MDYwMDQ5MDgxNjIwMDAwZDg5MTkwNjIwMDA1NDA1NjViNTAzNDgwMTU2MjAwMDBlNjU3NjAwMDgwZmQ1YjUwNjAwMTgwNjAwMDgwNjAwNjgxMTExNTYyMDAwMTAyNTc2MjAwMDEwMTYyMDAwNjI3NTY1YjViNjAwNjgxMTExNTYyMDAwMTE3NTc2MjAwMDExNjYyMDAwNjI3NTY1YjViODE1MjYwMjAwMTkwODE1MjYwMjAwMTYwMDAyMDgxOTA1NTUwNjAwMjYwMDE2MDAwNjAwMTYwMDY4MTExMTU2MjAwMDE0NjU3NjIwMDAxNDU2MjAwMDYyNzU2NWI1YjYwMDY4MTExMTU2MjAwMDE1YjU3NjIwMDAxNWE2MjAwMDYyNzU2NWI1YjgxNTI2MDIwMDE5MDgxNTI2MDIwMDE2MDAwMjA4MTkwNTU1MDYwMDQ2MDAxNjAwMDYwMDI2MDA2ODExMTE1NjIwMDAxOGE1NzYyMDAwMTg5NjIwMDA2Mjc1NjViNWI2MDA2ODExMTE1NjIwMDAxOWY1NzYyMDAwMTllNjIwMDA2Mjc1NjViNWI4MTUyNjAyMDAxOTA4MTUyNjAyMDAxNjAwMDIwODE5MDU1NTA2MDA4NjAwMTYwMDA2MDAzNjAwNjgxMTExNTYyMDAwMWNlNTc2MjAwMDFjZDYyMDAwNjI3NTY1YjViNjAwNjgxMTExNTYyMDAwMWUzNTc2MjAwMDFlMjYyMDAwNjI3NTY1YjViODE1MjYwMjAwMTkwODE1MjYwMjAwMTYwMDAyMDgxOTA1NTUwNjAxMDYwMDE2MDAwNjAwNDYwMDY4MTExMTU2MjAwMDIxMjU3NjIwMDAyMTE2MjAwMDYyNzU2NWI1YjYwMDY4MTExMTU2MjAwMDIyNzU3NjIwMDAyMjY2MjAwMDYyNzU2NWI1YjgxNTI2MDIwMDE5MDgxNTI2MDIwMDE2MDAwMjA4MTkwNTU1MDYwMjA2MDAxNjAwMDYwMDU2MDA2ODExMTE1NjIwMDAyNTY1NzYyMDAwMjU1NjIwMDA2Mjc1NjViNWI2MDA2ODExMTE1NjIwMDAyNmI1NzYyMDAwMjZhNjIwMDA2Mjc1NjViNWI4MTUyNjAyMDAxOTA4MTUyNjAyMDAxNjAwMDIwODE5MDU1NTA2MDQwNjAwMTYwMDA2MDA2ODA4MTExMTU2MjAwMDI5OTU3NjIwMDAyOTg2MjAwMDYyNzU2NWI1YjYwMDY4MTExMTU2MjAwMDJhZTU3NjIwMDAyYWQ2MjAwMDYyNzU2NWI1YjgxNTI2MDIwMDE5MDgxNTI2MDIwMDE2MDAwMjA4MTkwNTU1MDYyMDAwNjU2NTY1YjYwMDA4MTUxOTA1MDkxOTA1MDU2NWI3ZjRlNDg3YjcxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA2MDAwNTI2MDQxNjAwNDUyNjAyNDYwMDBmZDViN2Y0ZTQ4N2I3MTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDUyNjAyMjYwMDQ1MjYwMjQ2MDAwZmQ1YjYwMDA2MDAyODIwNDkwNTA2MDAxODIxNjgwNjIwMDAzNDg1NzYwN2Y4MjE2OTE1MDViNjAyMDgyMTA4MTAzNjIwMDAzNWU1NzYyMDAwMzVkNjIwMDAzMDA1NjViNWI1MDkxOTA1MDU2NWI2MDAwODE5MDUwODE2MDAwNTI2MDIwNjAwMDIwOTA1MDkxOTA1MDU2NWI2MDAwNjAyMDYwMWY4MzAxMDQ5MDUwOTE5MDUwNTY1YjYwMDA4MjgyMWI5MDUwOTI5MTUwNTA1NjViNjAwMDYwMDg4MzAyNjIwMDAzYzg3ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmY4MjYyMDAwMzg5NTY1YjYyMDAwM2Q0ODY4MzYyMDAwMzg5NTY1Yjk1NTA4MDE5ODQxNjkzNTA4MDg2MTY4NDE3OTI1MDUwNTA5MzkyNTA1MDUwNTY1YjYwMDA4MTkwNTA5MTkwNTA1NjViNjAwMDgxOTA1MDkxOTA1MDU2NWI2MDAwNjIwMDA0MjE2MjAwMDQxYjYyMDAwNDE1ODQ2MjAwMDNlYzU2NWI2MjAwMDNmNjU2NWI2MjAwMDNlYzU2NWI5MDUwOTE5MDUwNTY1YjYwMDA4MTkwNTA5MTkwNTA1NjViNjIwMDA0M2Q4MzYyMDAwNDAwNTY1YjYyMDAwNDU1NjIwMDA0NGM4MjYyMDAwNDI4NTY1Yjg0ODQ1NDYyMDAwMzk2NTY1YjgyNTU1MDUwNTA1MDU2NWI2MDAwOTA1NjViNjIwMDA0NmM2MjAwMDQ1ZDU2NWI2MjAwMDQ3OTgxODQ4NDYyMDAwNDMyNTY1YjUwNTA1MDU2NWI1YjgxODExMDE1NjIwMDA0YTE1NzYyMDAwNDk1NjAwMDgyNjIwMDA0NjI1NjViNjAwMTgxMDE5MDUwNjIwMDA0N2Y1NjViNTA1MDU2NWI2MDFmODIxMTE1NjIwMDA0ZjA1NzYyMDAwNGJhODE2MjAwMDM2NDU2NWI2MjAwMDRjNTg0NjIwMDAzNzk1NjViODEwMTYwMjA4NTEwMTU2MjAwMDRkNTU3ODE5MDUwNWI2MjAwMDRlZDYyMDAwNGU0ODU2MjAwMDM3OTU2NWI4MzAxODI2MjAwMDQ3ZTU2NWI1MDUwNWI1MDUwNTA1NjViNjAwMDgyODIxYzkwNTA5MjkxNTA1MDU2NWI2MDAwNjIwMDA1MTU2MDAwMTk4NDYwMDgwMjYyMDAwNGY1NTY1YjE5ODA4MzE2OTE1MDUwOTI5MTUwNTA1NjViNjAwMDYyMDAwNTMwODM4MzYyMDAwNTAyNTY1YjkxNTA4MjYwMDIwMjgyMTc5MDUwOTI5MTUwNTA1NjViNjIwMDA1NGI4MjYyMDAwMmM2NTY1YjY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYyMDAwNTY3NTc2MjAwMDU2NjYyMDAwMmQxNTY1YjViNjIwMDA1NzM4MjU0NjIwMDAzMmY1NjViNjIwMDA1ODA4MjgyODU2MjAwMDRhNTU2NWI2MDAwNjAyMDkwNTA2MDFmODMxMTYwMDE4MTE0NjIwMDA1Yjg1NzYwMDA4NDE1NjIwMDA1YTM1NzgyODcwMTUxOTA1MDViNjIwMDA1YWY4NTgyNjIwMDA1MjI1NjViODY1NTUwNjIwMDA2MWY1NjViNjAxZjE5ODQxNjYyMDAwNWM4ODY2MjAwMDM2NDU2NWI2MDAwNWI4MjgxMTAxNTYyMDAwNWYyNTc4NDg5MDE1MTgyNTU2MDAxODIwMTkxNTA2MDIwODUwMTk0NTA2MDIwODEwMTkwNTA2MjAwMDVjYjU2NWI4NjgzMTAxNTYyMDAwNjEyNTc4NDg5MDE1MTYyMDAwNjBlNjAxZjg5MTY4MjYyMDAwNTAyNTY1YjgzNTU1MDViNjAwMTYwMDI4ODAyMDE4ODU1NTA1MDUwNWI1MDUwNTA1MDUwNTA1NjViN2Y0ZTQ4N2I3MTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDUyNjAyMTYwMDQ1MjYwMjQ2MDAwZmQ1YjYxMzk1NjgwNjIwMDA2NjY2MDAwMzk2MDAwZjNmZTYwODA2MDQwNTI2MDA0MzYxMDYxMDBjMjU3NjAwMDM1NjBlMDFjODA2MzdkY2JiYzcyMTE2MTAwN2Y1NzgwNjNlYWM2ZjNmZTExNjEwMDU5NTc4MDYzZWFjNmYzZmUxNDYxMDI1NDU3ODA2M2ViNTQ4ZWZmMTQ2MTAyOTE1NzgwNjNmMDA3Mjk5MDE0NjEwMmFkNTc4MDYzZjE3MzA3NjAxNDYxMDJjOTU3NjEwMGMyNTY1YjgwNjM3ZGNiYmM3MjE0NjEwMWRmNTc4MDYzOWIyM2QzZDkxNDYxMDFmYjU3ODA2M2U2MDhlMThkMTQ2MTAyMzg1NzYxMDBjMjU2NWI4MDYzMTFlMWZjMDcxNDYxMDBjNzU3ODA2MzE1ZGFjYmVhMTQ2MTAxMDQ1NzgwNjMzYWExMmVmNTE0NjEwMTQxNTc4MDYzNDEyOWI3ZGIxNDYxMDE1ZDU3ODA2MzYxOGRjNjVlMTQ2MTAxOWE1NzgwNjM3OGZlNzJhMTE0NjEwMWMzNTc1YjYwMDA4MGZkNWIzNDgwMTU2MTAwZDM1NzYwMDA4MGZkNWI1MDYxMDBlZTYwMDQ4MDM2MDM4MTAxOTA2MTAwZTk5MTkwNjEyMjQzNTY1YjYxMDJlNTU2NWI2MDQwNTE2MTAwZmI5MTkwNjEyMmM2NTY1YjYwNDA1MTgwOTEwMzkwZjM1YjM0ODAxNTYxMDExMDU3NjAwMDgwZmQ1YjUwNjEwMTJiNjAwNDgwMzYwMzgxMDE5MDYxMDEyNjkxOTA2MTIyNDM1NjViNjEwNDAxNTY1YjYwNDA1MTYxMDEzODkxOTA2MTIyYzY1NjViNjA0MDUxODA5MTAzOTBmMzViNjEwMTViNjAwNDgwMzYwMzgxMDE5MDYxMDE1NjkxOTA2MTI0NTM1NjViNjEwNTFmNTY1YjAwNWIzNDgwMTU2MTAxNjk1NzYwMDA4MGZkNWI1MDYxMDE4NDYwMDQ4MDM2MDM4MTAxOTA2MTAxN2Y5MTkwNjEyNTJkNTY1YjYxMDc3MDU2NWI2MDQwNTE2MTAxOTE5MTkwNjEyNjkzNTY1YjYwNDA1MTgwOTEwMzkwZjM=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwhVLTTLzY254motMqsxubGiDkTKnwJ+ExQeDV1dAy7EmTI7Ac3o2taB9lpDqniBQRGgwI/uCerQYQo4+6oAIiDwoJCMLgnq0GELEHEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQjD4J6tBhC3BxICGAISAhgDGPjR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxj0ByKAIDViMzQ4MDE1NjEwMWE2NTc2MDAwODBmZDViNTA2MTAxYzE2MDA0ODAzNjAzODEwMTkwNjEwMWJjOTE5MDYxMjZiNTU2NWI2MTA3YTc1NjViMDA1YjYxMDFkZDYwMDQ4MDM2MDM4MTAxOTA2MTAxZDg5MTkwNjEyNzExNTY1YjYxMDhjZTU2NWIwMDViNjEwMWY5NjAwNDgwMzYwMzgxMDE5MDYxMDFmNDkxOTA2MTI3YjA1NjViNjEwYTY1NTY1YjAwNWIzNDgwMTU2MTAyMDc1NzYwMDA4MGZkNWI1MDYxMDIyMjYwMDQ4MDM2MDM4MTAxOTA2MTAyMWQ5MTkwNjEyMjQzNTY1YjYxMGI4NTU2NWI2MDQwNTE2MTAyMmY5MTkwNjEyMmM2NTY1YjYwNDA1MTgwOTEwMzkwZjM1YjYxMDI1MjYwMDQ4MDM2MDM4MTAxOTA2MTAyNGQ5MTkwNjEyOGE0NTY1YjYxMGNhMzU2NWIwMDViMzQ4MDE1NjEwMjYwNTc2MDAwODBmZDViNTA2MTAyN2I2MDA0ODAzNjAzODEwMTkwNjEwMjc2OTE5MDYxMjI0MzU2NWI2MTBlODA1NjViNjA0MDUxNjEwMjg4OTE5MDYxMjJjNjU2NWI2MDQwNTE4MDkxMDM5MGYzNWI2MTAyYWI2MDA0ODAzNjAzODEwMTkwNjEwMmE2OTE5MDYxMmEwZjU2NWI2MTBmOWM1NjViMDA1YjYxMDJjNzYwMDQ4MDM2MDM4MTAxOTA2MTAyYzI5MTkwNjEyYWMyNTY1YjYxMTMzNDU2NWIwMDViNjEwMmUzNjAwNDgwMzYwMzgxMDE5MDYxMDJkZTkxOTA2MTJiMDI1NjViNjExNTg3NTY1YjAwNWI2MDAwODA2MDAwNjEwMTY3NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY2MzliMjNkM2Q5NjBlMDFiODg4ODg4ODg2MDQwNTE2MDI0MDE2MTAzMjI5NDkzOTI5MTkwNjEyYmJmNTY1YjYwNDA1MTYwMjA4MTgzMDMwMzgxNTI5MDYwNDA1MjkwN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE5MTY2MDIwODIwMTgwNTE3YmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmODM4MTgzMTYxNzgzNTI1MDUwNTA1MDYwNDA1MTYxMDM4YzkxOTA2MTJjNDA1NjViNjAwMDYwNDA1MTgwODMwMzgxODU1YWY0OTE1MDUwM2Q4MDYwMDA4MTE0NjEwM2M3NTc2MDQwNTE5MTUwNjAxZjE5NjAzZjNkMDExNjgyMDE2MDQwNTIzZDgyNTIzZDYwMDA2MDIwODQwMTNlNjEwM2NjNTY1YjYwNjA5MTUwNWI1MDkxNTA5MTUwODE2MTAzZGQ1NzYwMTU2MTAzZjI1NjViODA4MDYwMjAwMTkwNTE4MTAxOTA2MTAzZjE5MTkwNjEyYzkwNTY1YjViNjAwMzBiOTI1MDUwNTA5NDkzNTA1MDUwNTA1NjViNjAwMDgwNjAwMDYxMDE2NzczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2NjMxNWRhY2JlYTYwZTAxYjg4ODg4ODg4NjA0MDUxNjAyNDAxNjEwNDNlOTQ5MzkyOTE5MDYxMmJiZjU2NWI2MDQwNTE2MDIwODE4MzAzMDM4MTUyOTA2MDQwNTI5MDdiZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxOTE2NjAyMDgyMDE4MDUxN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjgzODE4MzE2MTc4MzUyNTA1MDUwNTA2MDQwNTE2MTA0YTg5MTkwNjEyYzQwNTY1YjYwMDA2MDQwNTE4MDgzMDM4MTYwMDA4NjVhZjE5MTUwNTAzZDgwNjAwMDgxMTQ2MTA0ZTU1NzYwNDA1MTkxNTA2MDFmMTk2MDNmM2QwMTE2ODIwMTYwNDA1MjNkODI1MjNkNjAwMDYwMjA4NDAxM2U2MTA0ZWE1NjViNjA2MDkxNTA1YjUwOTE1MDkxNTA4MTYxMDRmYjU3NjAxNTYxMDUxMDU2NWI4MDgwNjAyMDAxOTA1MTgxMDE5MDYxMDUwZjkxOTA2MTJjOTA1NjViNWI2MDAzMGI5MjUwNTA1MDk0OTM1MDUwNTA1MDU2NWI2MDAwNjAwNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMDUzYzU3NjEwNTNiNjEyMmZjNTY1YjViNjA0MDUxOTA4MDgyNTI4MDYwMjAwMjYwMjAwMTgyMDE2MDQwNTI4MDE1NjEwNTc1NTc4MTYwMjAwMTViNjEwNTYyNjEyMDcyNTY1YjgxNTI2MDIwMDE5MDYwMDE5MDAzOTA4MTYxMDU1YTU3OTA1MDViNTA5MDUwNjEwNTg3NjAwMDYwMDE2MDAyODk2MTE2Yzg1NjViODE2MDAwODE1MTgxMTA2MTA1OWI1NzYxMDU5YTYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTA1YjQ2MDAyNjAwMzgwODg2MTE2Yzg1NjViODE2MDAxODE1MTgxMTA2MTA1Yzg1NzYxMDVjNzYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTA1ZTA2MDA0NjAwMTg2NjExNzAxNTY1YjgxNjAwMjgxNTE4MTEwNjEwNWY0NTc2MTA1ZjM2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwNjEwNjBjNjAwNjYwMDE4NjYxMTcwMTU2NWI4MTYwMDM4MTUxODExMDYxMDYyMDU3NjEwNjFmNjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMDYzODYwMDU2MDA0ODY2MTE3MDE1NjViODE2MDA0ODE1MTgxMTA2MTA2NGM1NzYxMDY0YjYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTA2NWY2MTIwOTI1NjViNjAwMDgxNjAwMDAxOTA2MDA3MGI5MDgxNjAwNzBiODE1MjUwNTA4MzgxNjAyMDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwODI4MTYwNDAwMTkwNjAwNzBiOTA4MTYwMDcwYjgxNTI1MDUwNjEwNmM0NjEyMGNmNTY1Yjg4ODE2MDQwMDE5MDczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2OTA4MTczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2ODE1MjUwNTA4MjgxNjBlMDAxODE5MDUyNTA4MTgxNjEwMTAwMDE4MTkwNTI1MDYwMDA2MTA3MWI4YjgzNjExNzM4NTY1YjkwNTA2MDE2NjAwMzBiODExNDYxMDc2MzU3NjA0MDUxN2YwOGMzNzlhMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwODE1MjYwMDQwMTYxMDc1YTkwNjEyZDQ5NTY1YjYwNDA1MTgwOTEwMzkwZmQ1YjUwNTA1MDUwNTA1MDUwNTA1MDUwNTA1NjViNjEwNzc4NjEyMTNlNTY1YjYwMDA4MDYxMDc4NTg1ODU2MTE4NTA1NjViOTE1MDYwMDcwYjkxNTA2MDE2NjAwMzBiODIxNDYxMDc5YzU3NjAwMDgwZmQ1YjgwOTI1MDUwNTA5MjkxNTA1MDU2NWI2MDAwODA2MTAxNjc3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjYzNjE4ZGM2NWU2MGUwMWI4NTg1NjA0MDUxNjAyNDAxNjEwN2RlOTI5MTkwNjEyZGIzNTY1YjYwNDA1MTYwMjA4MTgzMDMwMzgxNTI5MDYwNDA1MjkwN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE5MTY2MDIwODIwMTgwNTE3YmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmODM4MTgzMTYxNzgzNTI1MDUwNTA1MDYwNDA1MTYxMDg0ODkxOTA2MTJjNDA1NjViNjAwMDYwNDA1MTgwODMwMzgxNjAwMDg2NWFmMTkxNTA1MDNkODA2MDAwODExNDYxMDg4NTU3NjA0MDUxOTE1MDYwMWYxOTYwM2YzZDAxMTY4MjAxNjA0MDUyM2Q4MjUyM2Q2MDAwNjAyMDg0MDEzZTYxMDg4YTU2NWI2MDYwOTE1MDViNTA5MTUwOTE1MDdmNGFmNDc4MGUwNmZlOGNiOWRmNjRiMDc5NGZhNmYwMTM5OWFmOTc5MTc1YmI5ODhlMzVlMGU1N2U1OTQ1NjdiYzgyODI2MDQwNTE2MTA4YzA5MjkxOTA2MTJkZjI1NjViNjA0MDUxODA5MTAzOTBhMTUwNTA1MDUwNTY1YjYwMDA2MDA1NjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEwOGViNTc2MTA4ZWE2MTIyZmM1NjViNWI2MDQwNTE5MDgwODI1MjgwNjAyMDAyNjAyMDAxODIwMTYwNDA1MjgwMTU2MTA5MjQ1NzgxNjAyMDAxNWI2MTA5MTE2MTIwNzI1NjViODE1MjYwMjAwMTkwNjAwMTkwMDM5MDgxNjEwOTA5NTc5MDUwNWI1MDkwNTA2MTA5MzY2MDAwNjAwMTYwMDI4NzYxMTZjODU2NWI4MTYwMDA4MTUxODExMDYxMDk0YTU3NjEwOTQ5NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMDk2MzYwMDI2MDAzODA4NjYxMTZjODU2NWI4MTYwMDE4MTUxODExMDYxMDk3NzU3NjEwOTc2NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMDk4ZjYwMDQ2MDAxODQ2MTE3MDE1NjViODE2MDAyODE1MTgxMTA2MTA5YTM=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwn2PltKGqXM1J42aHgrT67F/n0d/oGpnYxImcmyeljW9pMV4dkFRxaHX103DKW+dnGgsI/+CerQYQq/TUTiIPCgkIw+CerQYQtwcSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIA"},{"b64Body":"Cg8KCQjD4J6tBhC9BxICGAISAhgDGPjR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxj0ByKAIDU3NjEwOWEyNjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMDliYjYwMDY2MDAxODQ2MTE3MDE1NjViODE2MDAzODE1MTgxMTA2MTA5Y2Y1NzYxMDljZTYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTA5ZTc2MDA1NjAwNDg0NjExNzAxNTY1YjgxNjAwNDgxNTE4MTEwNjEwOWZiNTc2MTA5ZmE2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwNjAwMDYxMGExMjg2ODM2MTE5ODQ1NjViNjAwNzBiOTA1MDYwMTY2MDAzMGI4MTE0NjEwYTVkNTc2MDQwNTE3ZjA4YzM3OWEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA4MTUyNjAwNDAxNjEwYTU0OTA2MTJlNmU1NjViNjA0MDUxODA5MTAzOTBmZDViNTA1MDUwNTA1MDUwNTY1YjYwMDA2MDAxNjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEwYTgyNTc2MTBhODE2MTIyZmM1NjViNWI2MDQwNTE5MDgwODI1MjgwNjAyMDAyNjAyMDAxODIwMTYwNDA1MjgwMTU2MTBhYmI1NzgxNjAyMDAxNWI2MTBhYTg2MTIwNzI1NjViODE1MjYwMjAwMTkwNjAwMTkwMDM5MDgxNjEwYWEwNTc5MDUwNWI1MDkwNTA2MTBhYzY2MTIwNzI1NjViNjAwNDgxNjAwMDAxODE4MTUyNTA1MDYxMGFkOTYxMjEzZTU2NWIzMDgxNjAyMDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwNjAwMTgxNjAwMDAxOTAxNTE1OTA4MTE1MTU4MTUyNTA1MDgwODI2MDIwMDE4MTkwNTI1MDgxODM2MDAwODE1MTgxMTA2MTBiNDA1NzYxMGIzZjYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MDAwNjEwYjViMzA2MDAwODg4ODg4NjExYTljNTY1YjkwNTA2MDAwNjEwYjY5ODg4MzYxMTczODU2NWI5MDUwNjAxNjYwMDMwYjgxMTQ2MTBiN2I1NzYwMDA4MGZkNWI1MDUwNTA1MDUwNTA1MDUwNTY1YjYwMDA4MDYwMDA2MTAxNjc3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjYzOWIyM2QzZDk2MGUwMWI4ODg4ODg4ODYwNDA1MTYwMjQwMTYxMGJjMjk0OTM5MjkxOTA2MTJiYmY1NjViNjA0MDUxNjAyMDgxODMwMzAzODE1MjkwNjA0MDUyOTA3YmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTkxNjYwMjA4MjAxODA1MTdiZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmY4MzgxODMxNjE3ODM1MjUwNTA1MDUwNjA0MDUxNjEwYzJjOTE5MDYxMmM0MDU2NWI2MDAwNjA0MDUxODA4MzAzODE2MDAwODY1YWYxOTE1MDUwM2Q4MDYwMDA4MTE0NjEwYzY5NTc2MDQwNTE5MTUwNjAxZjE5NjAzZjNkMDExNjgyMDE2MDQwNTIzZDgyNTIzZDYwMDA2MDIwODQwMTNlNjEwYzZlNTY1YjYwNjA5MTUwNWI1MDkxNTA5MTUwODE2MTBjN2Y1NzYwMTU2MTBjOTQ1NjViODA4MDYwMjAwMTkwNTE4MTAxOTA2MTBjOTM5MTkwNjEyYzkwNTY1YjViNjAwMzBiOTI1MDUwNTA5NDkzNTA1MDUwNTA1NjViNjAwMDYwMDU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTBjYzA1NzYxMGNiZjYxMjJmYzU2NWI1YjYwNDA1MTkwODA4MjUyODA2MDIwMDI2MDIwMDE4MjAxNjA0MDUyODAxNTYxMGNmOTU3ODE2MDIwMDE1YjYxMGNlNjYxMjA3MjU2NWI4MTUyNjAyMDAxOTA2MDAxOTAwMzkwODE2MTBjZGU1NzkwNTA1YjUwOTA1MDYxMGQwYjYwMDA2MDAxNjAwMjhjNjExNmM4NTY1YjgxNjAwMDgxNTE4MTEwNjEwZDFmNTc2MTBkMWU2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwNjEwZDM4NjAwMjYwMDM4MDhiNjExNmM4NTY1YjgxNjAwMTgxNTE4MTEwNjEwZDRjNTc2MTBkNGI2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwNjEwZDY0NjAwNDYwMDE4OTYxMTcwMTU2NWI4MTYwMDI4MTUxODExMDYxMGQ3ODU3NjEwZDc3NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMGQ5MDYwMDY2MDAxODk2MTE3MDE1NjViODE2MDAzODE1MTgxMTA2MTBkYTQ1NzYxMGRhMzYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTBkYmM2MDA1NjAwNDg5NjExNzAxNTY1YjgxNjAwNDgxNTE4MTEwNjEwZGQwNTc2MTBkY2Y2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwODM2MDAyOTA4MTYxMGRlYTkxOTA2MTMwYTU1NjViNTA4MjYwMDM5MDgxNjEwZGZhOTE5MDYxMzBhNTU2NWI1MDgxNjAwNDkwODE2MTBlMGE5MTkwNjEzMGE1NTY1YjUwNjAwMDYxMGUxYjhiNjAwMDg5ODk4NjYxMWE5YzU2NWI5MDUwNjAwMDYxMGUyOThkODM2MTE3Mzg1NjViOTA1MDYwMTY2MDAzMGI4MTE0NjEwZTcxNTc2MDQwNTE3ZjA4YzM3OWEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA4MTUyNjAwNDAxNjEwZTY4OTA2MTJkNDk1NjViNjA0MDUxODA5MTAzOTBmZDViNTA1MDUwNTA1MDUwNTA1MDUwNTA1MDUwNTA1NjViNjAwMDgwNjAwMDYxMDE2NzczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2NjMxNWRhY2JlYTYwZTAxYjg4ODg4ODg4NjA0MDUxNjAyNDAxNjEwZWJkOTQ5MzkyOTE5MDYxMmJiZjU2NWI2MDQwNTE2MDIwODE4MzAzMDM4MTUyOTA2MDQwNTI5MDdiZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxOTE2NjAyMDgyMDE4MDUxN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjgzODE4MzE2MTc4MzUyNTA1MDUwNTA2MDQwNTE2MTBmMjc5MTkwNjEyYzQwNTY1YjYwMDA2MDQwNTE4MDgzMDM4MTg1NWFmNDkxNTA1MDNkODA2MDAwODExNDYxMGY2MjU3NjA0MDUxOTE1MDYwMWYxOTYwM2YzZDAxMTY4MjAxNjA0MDUyM2Q4MjUyM2Q2MDAwNjAyMDg0MDEzZTYxMGY2NzU2NWI2MDYwOTE1MDViNTA5MTUwOTE1MDgxNjEwZjc4NTc2MDE1NjEwZjhkNTY1YjgwODA2MDIwMDE5MDUxODEwMTkwNjEwZjhjOTE5MDYxMmM5MDU2NWI1YjYwMDMwYjkyNTA1MDUwOTQ5MzUwNTA1MDUwNTY1YjYwMDA2MDA1NjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEwZmI5NTc2MTBmYjg2MTIyZmM1NjViNWI2MDQwNTE5MDgwODI1MjgwNjAyMDAyNjAyMDAxODIwMTYwNDA1MjgwMTU2MTBmZjI1NzgxNjAyMDAxNWI2MTBmZGY2MTIwNzI1NjViODE1MjYwMjAwMTkwNjAwMTkwMDM5MDgxNjEwZmQ3NTc5MDUwNWI1MDkwNTA2MTEwMDQ2MDAwNjAwMTYwMDI4NzYxMTZjODU2NWI4MTYwMDA4MTUxODExMDYxMTAxODU3NjExMDE3NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMTAzMTYwMDI2MDAzODA4NjYxMTZjODU2NWI4MTYwMDE4MTUxODExMDYxMTA0NTU3NjExMDQ0NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMTA1ZDYwMDQ2MDAxODQ2MTE3MDE1NjViODE2MDAyODE1MTgxMTA2MTEwNzE1NzYxMTA3MDYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTEwODk2MDA2NjAwMTg0NjExNzAxNTY1YjgxNjAwMzgxNTE4MTEwNjExMDlkNTc2MTEwOWM2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwNjExMGI1NjAwNTYwMDQ4NDYxMTcwMTU2NWI4MTYwMDQ4MTUxODExMDYxMTBjOTU3NjExMGM4NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMTBkYzYxMjBjZjU2NWI4NTgxNjA0MDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwNjAwMjgwNTQ2MTExMjE5MDYxMmVjODU2NWI4MDYwMWYwMTYwMjA4MDkxMDQwMjYwMjAwMTYwNDA1MTkwODEwMTYwNDA1MjgwOTI5MTkwODE4MTUyNjAyMDAxODI4MDU0NjExMTRkOTA2MTJlYzg1NjViODAxNTYxMTE5YTU3ODA2MDFmMTA2MTExNmY1NzYxMDEwMDgwODM1NDA0MDI4MzUyOTE2MDIwMDE5MTYxMTE5YTU2NWI4MjAxOTE5MDYwMDA1MjYwMjA2MDAwMjA5MDViODE1NDgxNTI5MDYwMDEwMTkwNjAyMDAxODA4MzExNjExMTdkNTc4MjkwMDM2MDFmMTY4MjAxOTE=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwEhk2zVEnnuVb+6gUlHMcHnBOMGGpItiC94bSVheQS3h8CcV2GFC0odbPbyFkKcSoGgwI/+CerQYQy67hwQIiDwoJCMPgnq0GEL0HEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQjE4J6tBhDDBxICGAISAhgDGPfR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxj0ByKAIDViNTA1MDUwNTA1MDgxNjAwMDAxODE5MDUyNTA2MDAzODA1NDYxMTFiNDkwNjEyZWM4NTY1YjgwNjAxZjAxNjAyMDgwOTEwNDAyNjAyMDAxNjA0MDUxOTA4MTAxNjA0MDUyODA5MjkxOTA4MTgxNTI2MDIwMDE4MjgwNTQ2MTExZTA5MDYxMmVjODU2NWI4MDE1NjExMjJkNTc4MDYwMWYxMDYxMTIwMjU3NjEwMTAwODA4MzU0MDQwMjgzNTI5MTYwMjAwMTkxNjExMjJkNTY1YjgyMDE5MTkwNjAwMDUyNjAyMDYwMDAyMDkwNWI4MTU0ODE1MjkwNjAwMTAxOTA2MDIwMDE4MDgzMTE2MTEyMTA1NzgyOTAwMzYwMWYxNjgyMDE5MTViNTA1MDUwNTA1MDgxNjAyMDAxODE5MDUyNTA4MTgxNjBlMDAxODE5MDUyNTA2MDA0ODA1NDYxMTI1MDkwNjEyZWM4NTY1YjgwNjAxZjAxNjAyMDgwOTEwNDAyNjAyMDAxNjA0MDUxOTA4MTAxNjA0MDUyODA5MjkxOTA4MTgxNTI2MDIwMDE4MjgwNTQ2MTEyN2M5MDYxMmVjODU2NWI4MDE1NjExMmM5NTc4MDYwMWYxMDYxMTI5ZTU3NjEwMTAwODA4MzU0MDQwMjgzNTI5MTYwMjAwMTkxNjExMmM5NTY1YjgyMDE5MTkwNjAwMDUyNjAyMDYwMDAyMDkwNWI4MTU0ODE1MjkwNjAwMTAxOTA2MDIwMDE4MDgzMTE2MTEyYWM1NzgyOTAwMzYwMWYxNjgyMDE5MTViNTA1MDUwNTA1MDgxNjA2MDAxODE5MDUyNTA2MDAwNjExMmUyODg4MzYxMTczODU2NWI5MDUwNjAxNjYwMDMwYjgxMTQ2MTEzMmE1NzYwNDA1MTdmMDhjMzc5YTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgxNTI2MDA0MDE2MTEzMjE5MDYxMzFjMzU2NWI2MDQwNTE4MDkxMDM5MGZkNWI1MDUwNTA1MDUwNTA1MDUwNTY1YjYxMTMzYzYxMjBjZjU2NWI2MDAyODA1NDYxMTM0OTkwNjEyZWM4NTY1YjgwNjAxZjAxNjAyMDgwOTEwNDAyNjAyMDAxNjA0MDUxOTA4MTAxNjA0MDUyODA5MjkxOTA4MTgxNTI2MDIwMDE4MjgwNTQ2MTEzNzU5MDYxMmVjODU2NWI4MDE1NjExM2MyNTc4MDYwMWYxMDYxMTM5NzU3NjEwMTAwODA4MzU0MDQwMjgzNTI5MTYwMjAwMTkxNjExM2MyNTY1YjgyMDE5MTkwNjAwMDUyNjAyMDYwMDAyMDkwNWI4MTU0ODE1MjkwNjAwMTAxOTA2MDIwMDE4MDgzMTE2MTEzYTU1NzgyOTAwMzYwMWYxNjgyMDE5MTViNTA1MDUwNTA1MDgxNjAwMDAxODE5MDUyNTA2MDAzODA1NDYxMTNkYzkwNjEyZWM4NTY1YjgwNjAxZjAxNjAyMDgwOTEwNDAyNjAyMDAxNjA0MDUxOTA4MTAxNjA0MDUyODA5MjkxOTA4MTgxNTI2MDIwMDE4MjgwNTQ2MTE0MDg5MDYxMmVjODU2NWI4MDE1NjExNDU1NTc4MDYwMWYxMDYxMTQyYTU3NjEwMTAwODA4MzU0MDQwMjgzNTI5MTYwMjAwMTkxNjExNDU1NTY1YjgyMDE5MTkwNjAwMDUyNjAyMDYwMDAyMDkwNWI4MTU0ODE1MjkwNjAwMTAxOTA2MDIwMDE4MDgzMTE2MTE0Mzg1NzgyOTAwMzYwMWYxNjgyMDE5MTViNTA1MDUwNTA1MDgxNjAyMDAxODE5MDUyNTA4MTgxNjA0MDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwNjAwNDgwNTQ2MTE0YTc5MDYxMmVjODU2NWI4MDYwMWYwMTYwMjA4MDkxMDQwMjYwMjAwMTYwNDA1MTkwODEwMTYwNDA1MjgwOTI5MTkwODE4MTUyNjAyMDAxODI4MDU0NjExNGQzOTA2MTJlYzg1NjViODAxNTYxMTUyMDU3ODA2MDFmMTA2MTE0ZjU1NzYxMDEwMDgwODM1NDA0MDI4MzUyOTE2MDIwMDE5MTYxMTUyMDU2NWI4MjAxOTE5MDYwMDA1MjYwMjA2MDAwMjA5MDViODE1NDgxNTI5MDYwMDEwMTkwNjAyMDAxODA4MzExNjExNTAzNTc4MjkwMDM2MDFmMTY4MjAxOTE1YjUwNTA1MDUwNTA4MTYwNjAwMTgxOTA1MjUwNjAwMDYxMTUzOTg0ODM2MTE3Mzg1NjViOTA1MDYwMTY2MDAzMGI4MTE0NjExNTgxNTc2MDQwNTE3ZjA4YzM3OWEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA4MTUyNjAwNDAxNjExNTc4OTA2MTMyNTU1NjViNjA0MDUxODA5MTAzOTBmZDViNTA1MDUwNTA1NjViNjExNThmNjEyMGNmNTY1YjgyODE2MDAwMDE4MTkwNTI1MDgxODE2MDIwMDE4MTkwNTI1MDgzODE2MDQwMDE5MDczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2OTA4MTczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2ODE1MjUwNTA2MDA0ODA1NDYxMTVlNjkwNjEyZWM4NTY1YjgwNjAxZjAxNjAyMDgwOTEwNDAyNjAyMDAxNjA0MDUxOTA4MTAxNjA0MDUyODA5MjkxOTA4MTgxNTI2MDIwMDE4MjgwNTQ2MTE2MTI5MDYxMmVjODU2NWI4MDE1NjExNjVmNTc4MDYwMWYxMDYxMTYzNDU3NjEwMTAwODA4MzU0MDQwMjgzNTI5MTYwMjAwMTkxNjExNjVmNTY1YjgyMDE5MTkwNjAwMDUyNjAyMDYwMDAyMDkwNWI4MTU0ODE1MjkwNjAwMTAxOTA2MDIwMDE4MDgzMTE2MTE2NDI1NzgyOTAwMzYwMWYxNjgyMDE5MTViNTA1MDUwNTA1MDgxNjA2MDAxODE5MDUyNTA2MDAwNjExNjc4ODY4MzYxMTczODU2NWI5MDUwNjAxNjYwMDMwYjgxMTQ2MTE2YzA1NzYwNDA1MTdmMDhjMzc5YTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgxNTI2MDA0MDE2MTE2Yjc5MDYxMzJlNzU2NWI2MDQwNTE4MDkxMDM5MGZkNWI1MDUwNTA1MDUwNTA1NjViNjExNmQwNjEyMDcyNTY1YjYwNDA1MTgwNjA0MDAxNjA0MDUyODA2MTE2ZTU4Nzg3NjExZDE2NTY1YjgxNTI2MDIwMDE2MTE2ZjQ4NTg1NjExZDZjNTY1YjgxNTI1MDkwNTA5NDkzNTA1MDUwNTA1NjViNjExNzA5NjEyMDcyNTY1YjYwNDA1MTgwNjA0MDAxNjA0MDUyODA2MTE3MWQ4NjYxMWY0MTU2NWI4MTUyNjAyMDAxNjExNzJjODU4NTYxMWY4MjU2NWI4MTUyNTA5MDUwOTM5MjUwNTA1MDU2NWI2MDAwODA2MDAwNjEwMTY3NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY2MzdkMzA1Y2ZhNjBlMDFiODY4NjYwNDA1MTYwMjQwMTYxMTc3MTkyOTE5MDYxMzYwNzU2NWI2MDQwNTE2MDIwODE4MzAzMDM4MTUyOTA2MDQwNTI5MDdiZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxOTE2NjAyMDgyMDE4MDUxN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjgzODE4MzE2MTc4MzUyNTA1MDUwNTA2MDQwNTE2MTE3ZGI5MTkwNjEyYzQwNTY1YjYwMDA2MDQwNTE4MDgzMDM4MTYwMDA4NjVhZjE5MTUwNTAzZDgwNjAwMDgxMTQ2MTE4MTg1NzYwNDA1MTkxNTA2MDFmMTk2MDNmM2QwMTE2ODIwMTYwNDA1MjNkODI1MjNkNjAwMDYwMjA4NDAxM2U2MTE4MWQ1NjViNjA2MDkxNTA1YjUwOTE1MDkxNTA4MTYxMTgyZTU3NjAxNTYxMTg0MzU2NWI4MDgwNjAyMDAxOTA1MTgxMDE5MDYxMTg0MjkxOTA2MTJjOTA1NjViNWI2MDAzMGI5MjUwNTA1MDkyOTE1MDUwNTY1YjYwMDA2MTE4NWE2MTIxM2U1NjViNjAwMDgwNjEwMTY3NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY2MzNjNGRkMzJlNjBlMDFiODc4NzYwNDA1MTYwMjQwMTYxMTg5MTkyOTE5MDYxMzYzNzU2NWI2MDQwNTE2MDIwODE4MzAzMDM4MTUyOTA2MDQwNTI5MDdiZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxOTE2NjAyMDgyMDE4MDUxN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjgzODE4MzE2MTc4MzUyNTA1MDUwNTA2MDQwNTE2MTE4ZmI5MTkwNjEyYzQwNTY1YjYwMDA2MDQwNTE4MDgzMDM4MTYwMDA4NjVhZjE5MTUwNTAzZDgwNjAwMDgxMTQ2MTE5Mzg1NzYwNDA1MTkxNTA2MDFmMTk2MDNmM2QwMTE2ODIwMTYwNDA1MjNkODI1MjNkNjAwMDYwMjA4NDAxM2U2MTE5M2Q1NjViNjA2MDkxNTA1YjUwOTE1MDkxNTA2MTE5NGE2MTIxM2U1NjViODI2MTE5NTc1NzYwMTU4MTYxMTk2YzU2NWI4MTgwNjAyMDAxOTA1MTgxMDE5MDYxMTk2YjkxOTA2MTM3ZGY1NjViNWI4MTYwMDMwYjkxNTA4MDk1NTA4MTk2NTA1MDUwNTA1MDUwOTI1MDkyOTA1MDU2NWI2MDAwODA2MDAwNjEwMTY3NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmY=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwoMAI+cQVfzsUaOHe7SetHksDS/96B6oCB14wjhqo6iDJJqhXDT5i6rzZRc1UqUXNGgsIgOGerQYQs7naSSIPCgkIxOCerQYQwwcSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIA"},{"b64Body":"Cg8KCQjE4J6tBhDJBxICGAISAhgDGPfR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxj0ByKAIGZmZmZmZmZmZmZmZmZmZmYxNjYzNmZjM2NiYWY2MGUwMWI4Njg2NjA0MDUxNjAyNDAxNjExOWJkOTI5MTkwNjEzOGMxNTY1YjYwNDA1MTYwMjA4MTgzMDMwMzgxNTI5MDYwNDA1MjkwN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE5MTY2MDIwODIwMTgwNTE3YmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmODM4MTgzMTYxNzgzNTI1MDUwNTA1MDYwNDA1MTYxMWEyNzkxOTA2MTJjNDA1NjViNjAwMDYwNDA1MTgwODMwMzgxNjAwMDg2NWFmMTkxNTA1MDNkODA2MDAwODExNDYxMWE2NDU3NjA0MDUxOTE1MDYwMWYxOTYwM2YzZDAxMTY4MjAxNjA0MDUyM2Q4MjUyM2Q2MDAwNjAyMDg0MDEzZTYxMWE2OTU2NWI2MDYwOTE1MDViNTA5MTUwOTE1MDgxNjExYTdhNTc2MDE1NjExYThmNTY1YjgwODA2MDIwMDE5MDUxODEwMTkwNjExYThlOTE5MDYxMmM5MDU2NWI1YjYwMDMwYjkyNTA1MDUwOTI5MTUwNTA1NjViNjExYWE0NjEyMGNmNTY1YjYxMWFhYzYxMjA5MjU2NWI4NTgxNjAwMDAxOTA2MDA3MGI5MDgxNjAwNzBiODE1MjUwNTA4NDgxNjAyMDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwODM4MTYwNDAwMTkwNjAwNzBiOTA4MTYwMDcwYjgxNTI1MDUwNjAwMjgwNTQ2MTFiMTU5MDYxMmVjODU2NWI4MDYwMWYwMTYwMjA4MDkxMDQwMjYwMjAwMTYwNDA1MTkwODEwMTYwNDA1MjgwOTI5MTkwODE4MTUyNjAyMDAxODI4MDU0NjExYjQxOTA2MTJlYzg1NjViODAxNTYxMWI4ZTU3ODA2MDFmMTA2MTFiNjM1NzYxMDEwMDgwODM1NDA0MDI4MzUyOTE2MDIwMDE5MTYxMWI4ZTU2NWI4MjAxOTE5MDYwMDA1MjYwMjA2MDAwMjA5MDViODE1NDgxNTI5MDYwMDEwMTkwNjAyMDAxODA4MzExNjExYjcxNTc4MjkwMDM2MDFmMTY4MjAxOTE1YjUwNTA1MDUwNTA4MjYwMDAwMTgxOTA1MjUwNjAwMzgwNTQ2MTFiYTg5MDYxMmVjODU2NWI4MDYwMWYwMTYwMjA4MDkxMDQwMjYwMjAwMTYwNDA1MTkwODEwMTYwNDA1MjgwOTI5MTkwODE4MTUyNjAyMDAxODI4MDU0NjExYmQ0OTA2MTJlYzg1NjViODAxNTYxMWMyMTU3ODA2MDFmMTA2MTFiZjY1NzYxMDEwMDgwODM1NDA0MDI4MzUyOTE2MDIwMDE5MTYxMWMyMTU2NWI4MjAxOTE5MDYwMDA1MjYwMjA2MDAwMjA5MDViODE1NDgxNTI5MDYwMDEwMTkwNjAyMDAxODA4MzExNjExYzA0NTc4MjkwMDM2MDFmMTY4MjAxOTE1YjUwNTA1MDUwNTA4MjYwMjAwMTgxOTA1MjUwODY4MjYwNDAwMTkwNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY5MDgxNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTUyNTA1MDgyODI2MGUwMDE4MTkwNTI1MDgwODI2MTAxMDAwMTgxOTA1MjUwNjAwNDgwNTQ2MTFjODY5MDYxMmVjODU2NWI4MDYwMWYwMTYwMjA4MDkxMDQwMjYwMjAwMTYwNDA1MTkwODEwMTYwNDA1MjgwOTI5MTkwODE4MTUyNjAyMDAxODI4MDU0NjExY2IyOTA2MTJlYzg1NjViODAxNTYxMWNmZjU3ODA2MDFmMTA2MTFjZDQ1NzYxMDEwMDgwODM1NDA0MDI4MzUyOTE2MDIwMDE5MTYxMWNmZjU2NWI4MjAxOTE5MDYwMDA1MjYwMjA2MDAwMjA5MDViODE1NDgxNTI5MDYwMDEwMTkwNjAyMDAxODA4MzExNjExY2UyNTc4MjkwMDM2MDFmMTY4MjAxOTE1YjUwNTA1MDUwNTA4MjYwNjAwMTgxOTA1MjUwNTA5NTk0NTA1MDUwNTA1MDU2NWI2MDAwNjExZDNkODM2MDA2ODExMTE1NjExZDJlNTc2MTFkMmQ2MTM4ZjE1NjViNWI4MjYxMjA1ZTkwOTE5MDYzZmZmZmZmZmYxNjU2NWI5MDUwNjExZDY0ODI2MDA2ODExMTE1NjExZDU1NTc2MTFkNTQ2MTM4ZjE1NjViNWI4MjYxMjA1ZTkwOTE5MDYzZmZmZmZmZmYxNjU2NWI5MDUwOTI5MTUwNTA1NjViNjExZDc0NjEyMTNlNTY1YjYwMDA2MDA0ODExMTE1NjExZDg4NTc2MTFkODc2MTM4ZjE1NjViNWI4MzYwMDQ4MTExMTU2MTFkOWI1NzYxMWQ5YTYxMzhmMTU2NWI1YjAzNjExZGI2NTc2MDAxODE2MDAwMDE5MDE1MTU5MDgxMTUxNTgxNTI1MDUwNjExZjNiNTY1YjYwMDE2MDA0ODExMTE1NjExZGNhNTc2MTFkYzk2MTM4ZjE1NjViNWI4MzYwMDQ4MTExMTU2MTFkZGQ1NzYxMWRkYzYxMzhmMTU2NWI1YjAzNjExZTNmNTc2MDAwODA1NDkwNjEwMTAwMGE5MDA0NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTYwMjAwMTkwNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY5MDgxNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTUyNTA1MDYxMWYzYTU2NWI2MDAyNjAwNDgxMTExNTYxMWU1MzU3NjExZTUyNjEzOGYxNTY1YjViODM2MDA0ODExMTE1NjExZTY2NTc2MTFlNjU2MTM4ZjE1NjViNWIwMzYxMWU3OTU3ODE4MTYwNDAwMTgxOTA1MjUwNjExZjM5NTY1YjYwMDM2MDA0ODExMTE1NjExZThkNTc2MTFlOGM2MTM4ZjE1NjViNWI4MzYwMDQ4MTExMTU2MTFlYTA1NzYxMWU5ZjYxMzhmMTU2NWI1YjAzNjExZWIzNTc4MTgxNjA2MDAxODE5MDUyNTA2MTFmMzg1NjViNjAwNDgwODExMTE1NjExZWM2NTc2MTFlYzU2MTM4ZjE1NjViNWI4MzYwMDQ4MTExMTU2MTFlZDk1NzYxMWVkODYxMzhmMTU2NWI1YjAzNjExZjM3NTc2MDAwODA1NDkwNjEwMTAwMGE5MDA0NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTYwODAwMTkwNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY5MDgxNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTUyNTA1MDViNWI1YjViNWI5MjkxNTA1MDU2NWI2MDAwNjAwMTYwMDA4MzYwMDY4MTExMTU2MTFmNWE1NzYxMWY1OTYxMzhmMTU2NWI1YjYwMDY4MTExMTU2MTFmNmM1NzYxMWY2YjYxMzhmMTU2NWI1YjgxNTI2MDIwMDE5MDgxNTI2MDIwMDE2MDAwMjA1NDkwNTA5MTkwNTA1NjViNjExZjhhNjEyMTNlNTY1YjYwMDE2MDA0ODExMTE1NjExZjllNTc2MTFmOWQ2MTM4ZjE1NjViNWI4MzYwMDQ4MTExMTU2MTFmYjE1NzYxMWZiMDYxMzhmMTU2NWI1YjAzNjExZmYzNTc4MTgxNjAyMDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwNjEyMDU4NTY1YjYwMDQ4MDgxMTExNTYxMjAwNjU3NjEyMDA1NjEzOGYxNTY1YjViODM2MDA0ODExMTE1NjEyMDE5NTc2MTIwMTg2MTM4ZjE1NjViNWIwMzYxMjA1NzU3ODE4MTYwODAwMTkwNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY5MDgxNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTUyNTA1MDViNWI5MjkxNTA1MDU2NWI2MDAwODE2MGZmMTY2MDAxOTAxYjgzMTc5MDUwOTI5MTUwNTA1NjViNjA0MDUxODA2MDQwMDE2MDQwNTI4MDYwMDA4MTUyNjAyMDAxNjEyMDhjNjEyMTNlNTY1YjgxNTI1MDkwNTY1YjYwNDA1MTgwNjA2MDAxNjA0MDUyODA2MDAwNjAwNzBiODE1MjYwMjAwMTYwMDA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI2MDIwMDE2MDAwNjAwNzBiODE1MjUwOTA1NjViNjA0MDUxODA2MTAxMjAwMTYwNDA1MjgwNjA2MDgxNTI2MDIwMDE2MDYwODE1MjYwMjAwMTYwMDA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI2MDIwMDE2MDYwODE1MjYwMjAwMTYwMDAxNTE1ODE1MjYwMjAwMTYwMDA2MDA3MGI4MTUyNjAyMDAxNjAwMDE1MTU4MTUyNjAyMDAxNjA2MDgxNTI2MDIwMDE2MTIxMzg2MTIwOTI1NjViODE1MjUwOTA1NjViNjA0MDUxODA2MGEwMDE2MDQwNTI4MDYwMDAxNTE1ODE1MjYwMjAwMTYwMDA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI2MDIwMDE2MDYwODE1MjYwMjAwMTYwNjA4MTUyNjAyMDAxNjAwMDczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2ODE1MjUwOTA=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwJWL2A2F5/oWAJlMO0SewQjSpa42aKRfotdHU0vtWkZJUO6mQDVECfxpmzSbkM28mGgwIgOGerQYQs4729gIiDwoJCMTgnq0GEMkHEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQjF4J6tBhDPBxICGAISAhgDGPbR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxj0ByKAIDU2NWI2MDAwNjA0MDUxOTA1MDkwNTY1YjYwMDA4MGZkNWI2MDAwODBmZDViNjAwMDczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjgyMTY5MDUwOTE5MDUwNTY1YjYwMDA2MTIxZGE4MjYxMjFhZjU2NWI5MDUwOTE5MDUwNTY1YjYxMjFlYTgxNjEyMWNmNTY1YjgxMTQ2MTIxZjU1NzYwMDA4MGZkNWI1MDU2NWI2MDAwODEzNTkwNTA2MTIyMDc4MTYxMjFlMTU2NWI5MjkxNTA1MDU2NWI2MDAwODE5MDUwOTE5MDUwNTY1YjYxMjIyMDgxNjEyMjBkNTY1YjgxMTQ2MTIyMmI1NzYwMDA4MGZkNWI1MDU2NWI2MDAwODEzNTkwNTA2MTIyM2Q4MTYxMjIxNzU2NWI5MjkxNTA1MDU2NWI2MDAwODA2MDAwODA2MDgwODU4NzAzMTIxNTYxMjI1ZDU3NjEyMjVjNjEyMWE1NTY1YjViNjAwMDYxMjI2Yjg3ODI4ODAxNjEyMWY4NTY1Yjk0NTA1MDYwMjA2MTIyN2M4NzgyODgwMTYxMjFmODU2NWI5MzUwNTA2MDQwNjEyMjhkODc4Mjg4MDE2MTIxZjg1NjViOTI1MDUwNjA2MDYxMjI5ZTg3ODI4ODAxNjEyMjJlNTY1YjkxNTA1MDkyOTU5MTk0NTA5MjUwNTY1YjYwMDA4MTYwMDcwYjkwNTA5MTkwNTA1NjViNjEyMmMwODE2MTIyYWE1NjViODI1MjUwNTA1NjViNjAwMDYwMjA4MjAxOTA1MDYxMjJkYjYwMDA4MzAxODQ2MTIyYjc1NjViOTI5MTUwNTA1NjViNjAwMDgwZmQ1YjYwMDA4MGZkNWI2MDAwNjAxZjE5NjAxZjgzMDExNjkwNTA5MTkwNTA1NjViN2Y0ZTQ4N2I3MTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDUyNjA0MTYwMDQ1MjYwMjQ2MDAwZmQ1YjYxMjMzNDgyNjEyMmViNTY1YjgxMDE4MTgxMTA2N2ZmZmZmZmZmZmZmZmZmZmY4MjExMTcxNTYxMjM1MzU3NjEyMzUyNjEyMmZjNTY1YjViODA2MDQwNTI1MDUwNTA1NjViNjAwMDYxMjM2NjYxMjE5YjU2NWI5MDUwNjEyMzcyODI4MjYxMjMyYjU2NWI5MTkwNTA1NjViNjAwMDY3ZmZmZmZmZmZmZmZmZmZmZjgyMTExNTYxMjM5MjU3NjEyMzkxNjEyMmZjNTY1YjViNjEyMzliODI2MTIyZWI1NjViOTA1MDYwMjA4MTAxOTA1MDkxOTA1MDU2NWI4MjgxODMzNzYwMDA4MzgzMDE1MjUwNTA1MDU2NWI2MDAwNjEyM2NhNjEyM2M1ODQ2MTIzNzc1NjViNjEyMzVjNTY1YjkwNTA4MjgxNTI2MDIwODEwMTg0ODQ4NDAxMTExNTYxMjNlNjU3NjEyM2U1NjEyMmU2NTY1YjViNjEyM2YxODQ4Mjg1NjEyM2E4NTY1YjUwOTM5MjUwNTA1MDU2NWI2MDAwODI2MDFmODMwMTEyNjEyNDBlNTc2MTI0MGQ2MTIyZTE1NjViNWI4MTM1NjEyNDFlODQ4MjYwMjA4NjAxNjEyM2I3NTY1YjkxNTA1MDkyOTE1MDUwNTY1YjYxMjQzMDgxNjEyMmFhNTY1YjgxMTQ2MTI0M2I1NzYwMDA4MGZkNWI1MDU2NWI2MDAwODEzNTkwNTA2MTI0NGQ4MTYxMjQyNzU2NWI5MjkxNTA1MDU2NWI2MDAwODA2MDAwODA2MDAwODA2MDAwNjBlMDg4OGEwMzEyMTU2MTI0NzI1NzYxMjQ3MTYxMjFhNTU2NWI1YjYwMDA2MTI0ODA4YTgyOGIwMTYxMjFmODU2NWI5NzUwNTA2MDIwNjEyNDkxOGE4MjhiMDE2MTIxZjg1NjViOTY1MDUwNjA0MDg4MDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjRiMjU3NjEyNGIxNjEyMWFhNTY1YjViNjEyNGJlOGE4MjhiMDE2MTIzZjk1NjViOTU1MDUwNjA2MDg4MDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjRkZjU3NjEyNGRlNjEyMWFhNTY1YjViNjEyNGViOGE4MjhiMDE2MTIzZjk1NjViOTQ1MDUwNjA4MDYxMjRmYzhhODI4YjAxNjEyMWY4NTY1YjkzNTA1MDYwYTA2MTI1MGQ4YTgyOGIwMTYxMjFmODU2NWI5MjUwNTA2MGMwNjEyNTFlOGE4MjhiMDE2MTI0M2U1NjViOTE1MDUwOTI5NTk4OTE5NDk3NTA5Mjk1NTA1NjViNjAwMDgwNjA0MDgzODUwMzEyMTU2MTI1NDQ1NzYxMjU0MzYxMjFhNTU2NWI1YjYwMDA2MTI1NTI4NTgyODYwMTYxMjFmODU2NWI5MjUwNTA2MDIwNjEyNTYzODU4Mjg2MDE2MTIyMmU1NjViOTE1MDUwOTI1MDkyOTA1MDU2NWI2MDAwODExNTE1OTA1MDkxOTA1MDU2NWI2MTI1ODI4MTYxMjU2ZDU2NWI4MjUyNTA1MDU2NWI2MTI1OTE4MTYxMjFjZjU2NWI4MjUyNTA1MDU2NWI2MDAwODE1MTkwNTA5MTkwNTA1NjViNjAwMDgyODI1MjYwMjA4MjAxOTA1MDkyOTE1MDUwNTY1YjYwMDA1YjgzODExMDE1NjEyNWQxNTc4MDgyMDE1MTgxODQwMTUyNjAyMDgxMDE5MDUwNjEyNWI2NTY1YjYwMDA4NDg0MDE1MjUwNTA1MDUwNTY1YjYwMDA2MTI1ZTg4MjYxMjU5NzU2NWI2MTI1ZjI4MTg1NjEyNWEyNTY1YjkzNTA2MTI2MDI4MTg1NjAyMDg2MDE2MTI1YjM1NjViNjEyNjBiODE2MTIyZWI1NjViODQwMTkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MGEwODMwMTYwMDA4MzAxNTE2MTI2MmU2MDAwODYwMTgyNjEyNTc5NTY1YjUwNjAyMDgzMDE1MTYxMjY0MTYwMjA4NjAxODI2MTI1ODg1NjViNTA2MDQwODMwMTUxODQ4MjAzNjA0MDg2MDE1MjYxMjY1OTgyODI2MTI1ZGQ1NjViOTE1MDUwNjA2MDgzMDE1MTg0ODIwMzYwNjA4NjAxNTI2MTI2NzM4MjgyNjEyNWRkNTY1YjkxNTA1MDYwODA4MzAxNTE2MTI2ODg2MDgwODYwMTgyNjEyNTg4NTY1YjUwODA5MTUwNTA5MjkxNTA1MDU2NWI2MDAwNjAyMDgyMDE5MDUwODE4MTAzNjAwMDgzMDE1MjYxMjZhZDgxODQ2MTI2MTY1NjViOTA1MDkyOTE1MDUwNTY1YjYwMDA4MDYwNDA4Mzg1MDMxMjE1NjEyNmNjNTc2MTI2Y2I2MTIxYTU1NjViNWI2MDAwNjEyNmRhODU4Mjg2MDE2MTIxZjg1NjViOTI1MDUwNjAyMDgzMDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjZmYjU3NjEyNmZhNjEyMWFhNTY1YjViNjEyNzA3ODU4Mjg2MDE2MTIzZjk1NjViOTE1MDUwOTI1MDkyOTA1MDU2NWI2MDAwODA2MDAwODA2MDgwODU4NzAzMTIxNTYxMjcyYjU3NjEyNzJhNjEyMWE1NTY1YjViNjAwMDYxMjczOTg3ODI4ODAxNjEyMWY4NTY1Yjk0NTA1MDYwMjA4NTAxMzU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTI3NWE1NzYxMjc1OTYxMjFhYTU2NWI1YjYxMjc2Njg3ODI4ODAxNjEyM2Y5NTY1YjkzNTA1MDYwNDA4NTAxMzU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTI3ODc1NzYxMjc4NjYxMjFhYTU2NWI1YjYxMjc5Mzg3ODI4ODAxNjEyM2Y5NTY1YjkyNTA1MDYwNjA2MTI3YTQ4NzgyODgwMTYxMjFmODU2NWI5MTUwNTA5Mjk1OTE5NDUwOTI1MDU2NWI2MDAwODA2MDAwNjA2MDg0ODYwMzEyMTU2MTI3Yzk1NzYxMjdjODYxMjFhNTU2NWI1YjYwMDA2MTI3ZDc4NjgyODcwMTYxMjFmODU2NWI5MzUwNTA2MDIwNjEyN2U4ODY4Mjg3MDE2MTIxZjg1NjViOTI1MDUwNjA0MDYxMjdmOTg2ODI4NzAxNjEyNDNlNTY1YjkxNTA1MDkyNTA5MjUwOTI1NjViNjAwMDY3ZmZmZmZmZmZmZmZmZmZmZjgyMTExNTYxMjgxZTU3NjEyODFkNjEyMmZjNTY1YjViNjEyODI3ODI2MTIyZWI1NjViOTA1MDYwMjA4MTAxOTA1MDkxOTA1MDU2NWI2MDAwNjEyODQ3NjEyODQyODQ2MTI4MDM1NjViNjEyMzVjNTY1YjkwNTA4MjgxNTI2MDIwODEwMTg0ODQ4NDAxMTExNTYxMjg2MzU3NjEyODYyNjEyMmU2NTY1YjViNjEyODZlODQ4Mjg1NjEyM2E4NTY1YjUwOTM5MjUwNTA1MDU2NWI2MDAwODI2MDFmODMwMTEyNjEyODhiNTc2MTI4OGE2MTIyZTE1NjViNWI4MTM1NjEyODliODQ4MjYwMjA4NjAxNjEyODM0NTY1YjkxNTA1MDkyOTE1MDUwNTY1YjYwMDA4MDYwMDA4MDYwMDA4MDYwMDA4MDYwMDA4MDYxMDE0MDhiOGQwMzEyMTU2MTI4Yzg1NzYxMjhjNzYxMjFhNTU2NWI1YjYwMDA2MTI4ZDY4ZDgyOGUwMTYxMjFmODU2NWI5YTUwNTA2MDIwNjEyOGU3OGQ4MjhlMDE2MTIxZjg1NjViOTk1MDUwNjA0MDhiMDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjkwODU3NjEyOTA3NjEyMWFhNTY1YjViNjEyOTE0OGQ4MjhlMDE2MTIzZjk1NjViOTg1MDUwNjA2MDhiMDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjkzNTU3NjEyOTM0NjEyMWFhNTY1YjViNjEyOTQxOGQ4MjhlMDE2MTIzZjk1NjViOTc1MDUwNjA4MDYxMjk1MjhkODI4ZTAxNjEyMWY4NTY1Yjk2NTA1MDYwYTA2MTI5NjM4ZDgyOGUwMTYxMjFmODU2NWI5NTUwNTA2MGMwNjEyOTc0OGQ4MjhlMDE2MTI0M2U1NjViOTQ1MDUwNjBlMDhiMDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjk5NTU3NjEyOTk0NjEyMWFhNTY1YjViNjEyOWExOGQ=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIw8bCY/+xVUJ6M4kuKmtLjSwuwKv6j9Y9AgUpkHfVXB5z3fXMyyiiFvDHdERkr7+kbGgsIgeGerQYQi7b4fSIPCgkIxeCerQYQzwcSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIA"},{"b64Body":"Cg8KCQjF4J6tBhDVBxICGAISAhgDGPbR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxj0ByKAIDgyOGUwMTYxMjg3NjU2NWI5MzUwNTA2MTAxMDA4YjAxMzU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTI5YzM1NzYxMjljMjYxMjFhYTU2NWI1YjYxMjljZjhkODI4ZTAxNjEyODc2NTY1YjkyNTA1MDYxMDEyMDhiMDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjlmMTU3NjEyOWYwNjEyMWFhNTY1YjViNjEyOWZkOGQ4MjhlMDE2MTI4NzY1NjViOTE1MDUwOTI5NTk4OWI5MTk0OTc5YTUwOTI5NTk4NTA1NjViNjAwMDgwNjAwMDgwNjAwMDYwYTA4Njg4MDMxMjE1NjEyYTJiNTc2MTJhMmE2MTIxYTU1NjViNWI2MDAwNjEyYTM5ODg4Mjg5MDE2MTIxZjg1NjViOTU1MDUwNjAyMDYxMmE0YTg4ODI4OTAxNjEyMWY4NTY1Yjk0NTA1MDYwNDA4NjAxMzU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTJhNmI1NzYxMmE2YTYxMjFhYTU2NWI1YjYxMmE3Nzg4ODI4OTAxNjEyM2Y5NTY1YjkzNTA1MDYwNjA4NjAxMzU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTJhOTg1NzYxMmE5NzYxMjFhYTU2NWI1YjYxMmFhNDg4ODI4OTAxNjEyM2Y5NTY1YjkyNTA1MDYwODA2MTJhYjU4ODgyODkwMTYxMjFmODU2NWI5MTUwNTA5Mjk1NTA5Mjk1OTA5MzUwNTY1YjYwMDA4MDYwNDA4Mzg1MDMxMjE1NjEyYWQ5NTc2MTJhZDg2MTIxYTU1NjViNWI2MDAwNjEyYWU3ODU4Mjg2MDE2MTIxZjg1NjViOTI1MDUwNjAyMDYxMmFmODg1ODI4NjAxNjEyMWY4NTY1YjkxNTA1MDkyNTA5MjkwNTA1NjViNjAwMDgwNjAwMDgwNjA4MDg1ODcwMzEyMTU2MTJiMWM1NzYxMmIxYjYxMjFhNTU2NWI1YjYwMDA2MTJiMmE4NzgyODgwMTYxMjFmODU2NWI5NDUwNTA2MDIwNjEyYjNiODc4Mjg4MDE2MTIxZjg1NjViOTM1MDUwNjA0MDg1MDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMmI1YzU3NjEyYjViNjEyMWFhNTY1YjViNjEyYjY4ODc4Mjg4MDE2MTI4NzY1NjViOTI1MDUwNjA2MDg1MDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMmI4OTU3NjEyYjg4NjEyMWFhNTY1YjViNjEyYjk1ODc4Mjg4MDE2MTI4NzY1NjViOTE1MDUwOTI5NTkxOTQ1MDkyNTA1NjViNjEyYmFhODE2MTIxY2Y1NjViODI1MjUwNTA1NjViNjEyYmI5ODE2MTIyMGQ1NjViODI1MjUwNTA1NjViNjAwMDYwODA4MjAxOTA1MDYxMmJkNDYwMDA4MzAxODc2MTJiYTE1NjViNjEyYmUxNjAyMDgzMDE4NjYxMmJhMTU2NWI2MTJiZWU2MDQwODMwMTg1NjEyYmExNTY1YjYxMmJmYjYwNjA4MzAxODQ2MTJiYjA1NjViOTU5NDUwNTA1MDUwNTA1NjViNjAwMDgxOTA1MDkyOTE1MDUwNTY1YjYwMDA2MTJjMWE4MjYxMjU5NzU2NWI2MTJjMjQ4MTg1NjEyYzA0NTY1YjkzNTA2MTJjMzQ4MTg1NjAyMDg2MDE2MTI1YjM1NjViODA4NDAxOTE1MDUwOTI5MTUwNTA1NjViNjAwMDYxMmM0YzgyODQ2MTJjMGY1NjViOTE1MDgxOTA1MDkyOTE1MDUwNTY1YjYwMDA4MTYwMDMwYjkwNTA5MTkwNTA1NjViNjEyYzZkODE2MTJjNTc1NjViODExNDYxMmM3ODU3NjAwMDgwZmQ1YjUwNTY1YjYwMDA4MTUxOTA1MDYxMmM4YTgxNjEyYzY0NTY1YjkyOTE1MDUwNTY1YjYwMDA2MDIwODI4NDAzMTIxNTYxMmNhNjU3NjEyY2E1NjEyMWE1NTY1YjViNjAwMDYxMmNiNDg0ODI4NTAxNjEyYzdiNTY1YjkxNTA1MDkyOTE1MDUwNTY1YjdmNGU0ODdiNzEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDYwMDA1MjYwMzI2MDA0NTI2MDI0NjAwMGZkNWI2MDAwODI4MjUyNjAyMDgyMDE5MDUwOTI5MTUwNTA1NjViN2Y1NTcwNjQ2MTc0NjUyMDZmNjYyMDc0NmY2YjY1NmU0OTZlNjY2ZjIwNjY2MTY5NmM2NTY0MjEwMDAwMDAwMDAwNjAwMDgyMDE1MjUwNTY1YjYwMDA2MTJkMzM2MDFiODM2MTJjZWM1NjViOTE1MDYxMmQzZTgyNjEyY2ZkNTY1YjYwMjA4MjAxOTA1MDkxOTA1MDU2NWI2MDAwNjAyMDgyMDE5MDUwODE4MTAzNjAwMDgzMDE1MjYxMmQ2MjgxNjEyZDI2NTY1YjkwNTA5MTkwNTA1NjViNjAwMDgyODI1MjYwMjA4MjAxOTA1MDkyOTE1MDUwNTY1YjYwMDA2MTJkODU4MjYxMjU5NzU2NWI2MTJkOGY4MTg1NjEyZDY5NTY1YjkzNTA2MTJkOWY4MTg1NjAyMDg2MDE2MTI1YjM1NjViNjEyZGE4ODE2MTIyZWI1NjViODQwMTkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MDQwODIwMTkwNTA2MTJkYzg2MDAwODMwMTg1NjEyYmExNTY1YjgxODEwMzYwMjA4MzAxNTI2MTJkZGE4MTg0NjEyZDdhNTY1YjkwNTA5MzkyNTA1MDUwNTY1YjYxMmRlYzgxNjEyNTZkNTY1YjgyNTI1MDUwNTY1YjYwMDA2MDQwODIwMTkwNTA2MTJlMDc2MDAwODMwMTg1NjEyZGUzNTY1YjgxODEwMzYwMjA4MzAxNTI2MTJlMTk4MTg0NjEyZDdhNTY1YjkwNTA5MzkyNTA1MDUwNTY1YjdmNTU3MDY0NjE3NDY1MjA2ZjY2MjA3NDZmNmI2NTZlMjA2YjY1Nzk3MzIwNjY2MTY5NmM2NTY0MjEwMDAwMDAwMDYwMDA4MjAxNTI1MDU2NWI2MDAwNjEyZTU4NjAxYzgzNjEyY2VjNTY1YjkxNTA2MTJlNjM4MjYxMmUyMjU2NWI2MDIwODIwMTkwNTA5MTkwNTA1NjViNjAwMDYwMjA4MjAxOTA1MDgxODEwMzYwMDA4MzAxNTI2MTJlODc4MTYxMmU0YjU2NWI5MDUwOTE5MDUwNTY1YjYwMDA4MTUxOTA1MDkxOTA1MDU2NWI3ZjRlNDg3YjcxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA2MDAwNTI2MDIyNjAwNDUyNjAyNDYwMDBmZDViNjAwMDYwMDI4MjA0OTA1MDYwMDE4MjE2ODA2MTJlZTA1NzYwN2Y4MjE2OTE1MDViNjAyMDgyMTA4MTAzNjEyZWYzNTc2MTJlZjI2MTJlOTk1NjViNWI1MDkxOTA1MDU2NWI2MDAwODE5MDUwODE2MDAwNTI2MDIwNjAwMDIwOTA1MDkxOTA1MDU2NWI2MDAwNjAyMDYwMWY4MzAxMDQ5MDUwOTE5MDUwNTY1YjYwMDA4MjgyMWI5MDUwOTI5MTUwNTA1NjViNjAwMDYwMDg4MzAyNjEyZjViN2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmODI2MTJmMWU1NjViNjEyZjY1ODY4MzYxMmYxZTU2NWI5NTUwODAxOTg0MTY5MzUwODA4NjE2ODQxNzkyNTA1MDUwOTM5MjUwNTA1MDU2NWI2MDAwODE5MDUwOTE5MDUwNTY1YjYwMDA2MTJmYTI2MTJmOWQ2MTJmOTg4NDYxMjIwZDU2NWI2MTJmN2Q1NjViNjEyMjBkNTY1YjkwNTA5MTkwNTA1NjViNjAwMDgxOTA1MDkxOTA1MDU2NWI2MTJmYmM4MzYxMmY4NzU2NWI2MTJmZDA2MTJmYzg4MjYxMmZhOTU2NWI4NDg0NTQ2MTJmMmI1NjViODI1NTUwNTA1MDUwNTY1YjYwMDA5MDU2NWI2MTJmZTU2MTJmZDg1NjViNjEyZmYwODE4NDg0NjEyZmIzNTY1YjUwNTA1MDU2NWI1YjgxODExMDE1NjEzMDE0NTc2MTMwMDk2MDAwODI2MTJmZGQ1NjViNjAwMTgxMDE5MDUwNjEyZmY2NTY1YjUwNTA1NjViNjAxZjgyMTExNTYxMzA1OTU3NjEzMDJhODE2MTJlZjk1NjViNjEzMDMzODQ2MTJmMGU1NjViODEwMTYwMjA4NTEwMTU2MTMwNDI1NzgxOTA1MDViNjEzMDU2NjEzMDRlODU2MTJmMGU1NjViODMwMTgyNjEyZmY1NTY1YjUwNTA1YjUwNTA1MDU2NWI2MDAwODI4MjFjOTA1MDkyOTE1MDUwNTY1YjYwMDA2MTMwN2M2MDAwMTk4NDYwMDgwMjYxMzA1ZTU2NWIxOTgwODMxNjkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MTMwOTU4MzgzNjEzMDZiNTY1YjkxNTA4MjYwMDIwMjgyMTc5MDUwOTI5MTUwNTA1NjViNjEzMGFlODI2MTJlOGU1NjViNjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEzMGM3NTc2MTMwYzY2MTIyZmM1NjViNWI2MTMwZDE4MjU0NjEyZWM4NTY1YjYxMzBkYzgyODI4NTYxMzAxODU2NWI2MDAwNjAyMDkwNTA2MDFmODMxMTYwMDE4MTE0NjEzMTBmNTc2MDAwODQxNTYxMzBmZDU3ODI4NzAxNTE5MDUwNWI2MTMxMDc4NTgyNjEzMDg5NTY1Yjg2NTU1MDYxMzE2ZjU2NWI2MDFmMTk4NDE2NjEzMTFkODY2MTJlZjk1NjViNjAwMDViODI4MTEwMTU2MTMxNDU1Nzg0ODkwMTUxODI1NTYwMDE4MjAxOTE1MDYwMjA4NTAxOTQ1MDYwMjA4MTAxOTA1MDYxMzEyMDU2NWI4NjgzMTAxNTYxMzE2MjU3ODQ4OTAxNTE2MTMxNWU2MDFmODkxNjgyNjEzMDZiNTY1YjgzNTU1MDViNjAwMTYwMDI4ODAyMDE4ODU1NTA1MDUwNWI1MDUwNTA1MDUwNTA1NjViN2Y1NTcwNjQ2MTc0NjUyMDZmNjYyMDc0NmY2YjY1NmU0OTZlNjY2ZjIwNmI2NTc5NzMyMDY2NjE2OTZjNjU2NDIxNjA=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwvEToTPPAcXrDTTybLE6i0cSbtFYQvawW07MrI4jraTQVb6dd4rLgpzZLDuV2/vEoGgwIgeGerQYQk+/a/wIiDwoJCMXgnq0GENUHEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQjG4J6tBhDbBxICGAISAhgDGPKjnj4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBgB8SAxj0ByL4HjAwODIwMTUyNTA1NjViNjAwMDYxMzFhZDYwMjA4MzYxMmNlYzU2NWI5MTUwNjEzMWI4ODI2MTMxNzc1NjViNjAyMDgyMDE5MDUwOTE5MDUwNTY1YjYwMDA2MDIwODIwMTkwNTA4MTgxMDM2MDAwODMwMTUyNjEzMWRjODE2MTMxYTA1NjViOTA1MDkxOTA1MDU2NWI3ZjU1NzA2NDYxNzQ2NTIwNmY2NjIwNzQ2ZjZiNjU2ZTQ5NmU2NjZmMmU3NDcyNjU2MTczNzU3Mjc5MjA2NjYxNjk2MDAwODIwMTUyN2Y2YzY1NjQyMTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAyMDgyMDE1MjUwNTY1YjYwMDA2MTMyM2Y2MDI0ODM2MTJjZWM1NjViOTE1MDYxMzI0YTgyNjEzMWUzNTY1YjYwNDA4MjAxOTA1MDkxOTA1MDU2NWI2MDAwNjAyMDgyMDE5MDUwODE4MTAzNjAwMDgzMDE1MjYxMzI2ZTgxNjEzMjMyNTY1YjkwNTA5MTkwNTA1NjViN2Y1NTcwNjQ2MTc0NjUyMDZmNjYyMDc0NmY2YjY1NmU0OTZlNjY2ZjIwNmU2MTZkNjUyMDYxNmU2NDIwNzM3OTZkNjAwMDgyMDE1MjdmNjI2ZjZjMjA2NjYxNjk2YzY1NjQyMTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDYwMjA4MjAxNTI1MDU2NWI2MDAwNjEzMmQxNjAyYjgzNjEyY2VjNTY1YjkxNTA2MTMyZGM4MjYxMzI3NTU2NWI2MDQwODIwMTkwNTA5MTkwNTA1NjViNjAwMDYwMjA4MjAxOTA1MDgxODEwMzYwMDA4MzAxNTI2MTMzMDA4MTYxMzJjNDU2NWI5MDUwOTE5MDUwNTY1YjYwMDA4MjgyNTI2MDIwODIwMTkwNTA5MjkxNTA1MDU2NWI2MDAwNjEzMzIzODI2MTJlOGU1NjViNjEzMzJkODE4NTYxMzMwNzU2NWI5MzUwNjEzMzNkODE4NTYwMjA4NjAxNjEyNWIzNTY1YjYxMzM0NjgxNjEyMmViNTY1Yjg0MDE5MTUwNTA5MjkxNTA1MDU2NWI2MTMzNWE4MTYxMjJhYTU2NWI4MjUyNTA1MDU2NWI2MDAwODE1MTkwNTA5MTkwNTA1NjViNjAwMDgyODI1MjYwMjA4MjAxOTA1MDkyOTE1MDUwNTY1YjYwMDA4MTkwNTA2MDIwODIwMTkwNTA5MTkwNTA1NjViNjEzMzk1ODE2MTIyMGQ1NjViODI1MjUwNTA1NjViNjAwMDYwYTA4MzAxNjAwMDgzMDE1MTYxMzNiMzYwMDA4NjAxODI2MTI1Nzk1NjViNTA2MDIwODMwMTUxNjEzM2M2NjAyMDg2MDE4MjYxMjU4ODU2NWI1MDYwNDA4MzAxNTE4NDgyMDM2MDQwODYwMTUyNjEzM2RlODI4MjYxMjVkZDU2NWI5MTUwNTA2MDYwODMwMTUxODQ4MjAzNjA2MDg2MDE1MjYxMzNmODgyODI2MTI1ZGQ1NjViOTE1MDUwNjA4MDgzMDE1MTYxMzQwZDYwODA4NjAxODI2MTI1ODg1NjViNTA4MDkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MDQwODMwMTYwMDA4MzAxNTE2MTM0MzA2MDAwODYwMTgyNjEzMzhjNTY1YjUwNjAyMDgzMDE1MTg0ODIwMzYwMjA4NjAxNTI2MTM0NDg4MjgyNjEzMzliNTY1YjkxNTA1MDgwOTE1MDUwOTI5MTUwNTA1NjViNjAwMDYxMzQ2MTgzODM2MTM0MTg1NjViOTA1MDkyOTE1MDUwNTY1YjYwMDA2MDIwODIwMTkwNTA5MTkwNTA1NjViNjAwMDYxMzQ4MTgyNjEzMzYwNTY1YjYxMzQ4YjgxODU2MTMzNmI1NjViOTM1MDgzNjAyMDgyMDI4NTAxNjEzNDlkODU2MTMzN2M1NjViODA2MDAwNWI4NTgxMTAxNTYxMzRkOTU3ODQ4NDAzODk1MjgxNTE2MTM0YmE4NTgyNjEzNDU1NTY1Yjk0NTA2MTM0YzU4MzYxMzQ2OTU2NWI5MjUwNjAyMDhhMDE5OTUwNTA2MDAxODEwMTkwNTA2MTM0YTE1NjViNTA4Mjk3NTA4Nzk1NTA1MDUwNTA1MDUwOTI5MTUwNTA1NjViNjA2MDgyMDE2MDAwODIwMTUxNjEzNTAxNjAwMDg1MDE4MjYxMzM1MTU2NWI1MDYwMjA4MjAxNTE2MTM1MTQ2MDIwODUwMTgyNjEyNTg4NTY1YjUwNjA0MDgyMDE1MTYxMzUyNzYwNDA4NTAxODI2MTMzNTE1NjViNTA1MDUwNTA1NjViNjAwMDYxMDE2MDgzMDE2MDAwODMwMTUxODQ4MjAzNjAwMDg2MDE1MjYxMzU0YjgyODI2MTMzMTg1NjViOTE1MDUwNjAyMDgzMDE1MTg0ODIwMzYwMjA4NjAxNTI2MTM1NjU4MjgyNjEzMzE4NTY1YjkxNTA1MDYwNDA4MzAxNTE2MTM1N2E2MDQwODYwMTgyNjEyNTg4NTY1YjUwNjA2MDgzMDE1MTg0ODIwMzYwNjA4NjAxNTI2MTM1OTI4MjgyNjEzMzE4NTY1YjkxNTA1MDYwODA4MzAxNTE2MTM1YTc2MDgwODYwMTgyNjEyNTc5NTY1YjUwNjBhMDgzMDE1MTYxMzViYTYwYTA4NjAxODI2MTMzNTE1NjViNTA2MGMwODMwMTUxNjEzNWNkNjBjMDg2MDE4MjYxMjU3OTU2NWI1MDYwZTA4MzAxNTE4NDgyMDM2MGUwODYwMTUyNjEzNWU1ODI4MjYxMzQ3NjU2NWI5MTUwNTA2MTAxMDA4MzAxNTE2MTM1ZmM2MTAxMDA4NjAxODI2MTM0ZWI1NjViNTA4MDkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MDQwODIwMTkwNTA2MTM2MWM2MDAwODMwMTg1NjEyYmExNTY1YjgxODEwMzYwMjA4MzAxNTI2MTM2MmU4MTg0NjEzNTJkNTY1YjkwNTA5MzkyNTA1MDUwNTY1YjYwMDA2MDQwODIwMTkwNTA2MTM2NGM2MDAwODMwMTg1NjEyYmExNTY1YjYxMzY1OTYwMjA4MzAxODQ2MTJiYjA1NjViOTM5MjUwNTA1MDU2NWI2MDAwODBmZDViNjAwMDgwZmQ1YjYxMzY3MzgxNjEyNTZkNTY1YjgxMTQ2MTM2N2U1NzYwMDA4MGZkNWI1MDU2NWI2MDAwODE1MTkwNTA2MTM2OTA4MTYxMzY2YTU2NWI5MjkxNTA1MDU2NWI2MDAwODE1MTkwNTA2MTM2YTU4MTYxMjFlMTU2NWI5MjkxNTA1MDU2NWI2MDAwNjEzNmJlNjEzNmI5ODQ2MTIzNzc1NjViNjEyMzVjNTY1YjkwNTA4MjgxNTI2MDIwODEwMTg0ODQ4NDAxMTExNTYxMzZkYTU3NjEzNmQ5NjEyMmU2NTY1YjViNjEzNmU1ODQ4Mjg1NjEyNWIzNTY1YjUwOTM5MjUwNTA1MDU2NWI2MDAwODI2MDFmODMwMTEyNjEzNzAyNTc2MTM3MDE2MTIyZTE1NjViNWI4MTUxNjEzNzEyODQ4MjYwMjA4NjAxNjEzNmFiNTY1YjkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MGEwODI4NDAzMTIxNTYxMzczMTU3NjEzNzMwNjEzNjYwNTY1YjViNjEzNzNiNjBhMDYxMjM1YzU2NWI5MDUwNjAwMDYxMzc0Yjg0ODI4NTAxNjEzNjgxNTY1YjYwMDA4MzAxNTI1MDYwMjA2MTM3NWY4NDgyODUwMTYxMzY5NjU2NWI2MDIwODMwMTUyNTA2MDQwODIwMTUxNjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEzNzgzNTc2MTM3ODI2MTM2NjU1NjViNWI2MTM3OGY4NDgyODUwMTYxMzZlZDU2NWI2MDQwODMwMTUyNTA2MDYwODIwMTUxNjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEzN2IzNTc2MTM3YjI2MTM2NjU1NjViNWI2MTM3YmY4NDgyODUwMTYxMzZlZDU2NWI2MDYwODMwMTUyNTA2MDgwNjEzN2QzODQ4Mjg1MDE2MTM2OTY1NjViNjA4MDgzMDE1MjUwOTI5MTUwNTA1NjViNjAwMDgwNjA0MDgzODUwMzEyMTU2MTM3ZjY1NzYxMzdmNTYxMjFhNTU2NWI1YjYwMDA2MTM4MDQ4NTgyODYwMTYxMmM3YjU2NWI5MjUwNTA2MDIwODMwMTUxNjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEzODI1NTc2MTM4MjQ2MTIxYWE1NjViNWI2MTM4MzE4NTgyODYwMTYxMzcxYjU2NWI5MTUwNTA5MjUwOTI5MDUwNTY1YjYwMDA4MjgyNTI2MDIwODIwMTkwNTA5MjkxNTA1MDU2NWI2MDAwNjEzODU3ODI2MTMzNjA1NjViNjEzODYxODE4NTYxMzgzYjU2NWI5MzUwODM2MDIwODIwMjg1MDE2MTM4NzM4NTYxMzM3YzU2NWI4MDYwMDA1Yjg1ODExMDE1NjEzOGFmNTc4NDg0MDM4OTUyODE1MTYxMzg5MDg1ODI2MTM0NTU1NjViOTQ1MDYxMzg5YjgzNjEzNDY5NTY1YjkyNTA2MDIwOGEwMTk5NTA1MDYwMDE4MTAxOTA1MDYxMzg3NzU2NWI1MDgyOTc1MDg3OTU1MDUwNTA1MDUwNTA5MjkxNTA1MDU2NWI2MDAwNjA0MDgyMDE5MDUwNjEzOGQ2NjAwMDgzMDE4NTYxMmJhMTU2NWI4MTgxMDM2MDIwODMwMTUyNjEzOGU4ODE4NDYxMzg0YzU2NWI5MDUwOTM5MjUwNTA1MDU2NWI3ZjRlNDg3YjcxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA2MDAwNTI2MDIxNjAwNDUyNjAyNDYwMDBmZGZlYTI2NDY5NzA2NjczNTgyMjEyMjBlZDAxNzAwYmU1OTIxNmVjMDMxNjViOGEyMWZkOTVjY2NmOTQ3NmM1NmI5M2FiNTk5Nzg3MmE4ZGZkZWNiYTY0NjQ3MzZmNmM2MzQzMDAwODEyMDAzMw==","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwsVxbZxyZgDrvxoz9TixGzONvi4Zc6f52+SdtmaW13V7Q3ySt299pqy6CluOZC84JGgwIguGerQYQ686pnwEiDwoJCMbgnq0GENsHEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQjG4J6tBhDdBxICGAISAhgDGJb7rp0CIgIIeDIgw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5CRgoDGPQHGiISIL3fOlVtJMXaxiOg8DNWoQwOozkXPIQuH8XbgRZzDDkDIICS9AFCBQiAztoDUgBaAGoLY2VsbGFyIGRvb3I=","b64Record":"CiUIFiIDGPUHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjCpvkZW2uh4U56AVCp9zgji/QLh/vdjIYRBjcQucfwxxWRStXG7c2uerKd19eJgB0kaDAiC4Z6tBhDbr4KFAyIPCgkIxuCerQYQ3QcSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjjCAmKtsQox1CgMY9QcS1nJggGBAUmAENhBhAMJXYAA1YOAcgGN9y7xyEWEAf1eAY+rG8/4RYQBZV4Bj6sbz/hRhAlRXgGPrVI7/FGECkVeAY/AHKZAUYQKtV4Bj8XMHYBRhAslXYQDCVluAY33LvHIUYQHfV4BjmyPT2RRhAftXgGPmCOGNFGECOFdhAMJWW4BjEeH8BxRhAMdXgGMV2svqFGEBBFeAYzqhLvUUYQFBV4BjQSm32xRhAV1XgGNhjcZeFGEBmleAY3j+cqEUYQHDV1tgAID9WzSAFWEA01dgAID9W1BhAO5gBIA2A4EBkGEA6ZGQYSJDVlthAuVWW2BAUWEA+5GQYSLGVltgQFGAkQOQ81s0gBVhARBXYACA/VtQYQErYASANgOBAZBhASaRkGEiQ1ZbYQQBVltgQFFhATiRkGEixlZbYEBRgJEDkPNbYQFbYASANgOBAZBhAVaRkGEkU1ZbYQUfVlsAWzSAFWEBaVdgAID9W1BhAYRgBIA2A4EBkGEBf5GQYSUtVlthB3BWW2BAUWEBkZGQYSaTVltgQFGAkQOQ81s0gBVhAaZXYACA/VtQYQHBYASANgOBAZBhAbyRkGEmtVZbYQenVlsAW2EB3WAEgDYDgQGQYQHYkZBhJxFWW2EIzlZbAFthAflgBIA2A4EBkGEB9JGQYSewVlthCmVWWwBbNIAVYQIHV2AAgP1bUGECImAEgDYDgQGQYQIdkZBhIkNWW2ELhVZbYEBRYQIvkZBhIsZWW2BAUYCRA5DzW2ECUmAEgDYDgQGQYQJNkZBhKKRWW2EMo1ZbAFs0gBVhAmBXYACA/VtQYQJ7YASANgOBAZBhAnaRkGEiQ1ZbYQ6AVltgQFFhAoiRkGEixlZbYEBRgJEDkPNbYQKrYASANgOBAZBhAqaRkGEqD1ZbYQ+cVlsAW2ECx2AEgDYDgQGQYQLCkZBhKsJWW2ETNFZbAFthAuNgBIA2A4EBkGEC3pGQYSsCVlthFYdWWwBbYACAYABhAWdz//////////////////////////8WY5sj09lg4BuIiIiIYEBRYCQBYQMilJOSkZBhK79WW2BAUWAggYMDA4FSkGBAUpB7/////////////////////////////////////xkWYCCCAYBRe/////////////////////////////////////+DgYMWF4NSUFBQUGBAUWEDjJGQYSxAVltgAGBAUYCDA4GFWvSRUFA9gGAAgRRhA8dXYEBRkVBgHxlgPz0BFoIBYEBSPYJSPWAAYCCEAT5hA8xWW2BgkVBbUJFQkVCBYQPdV2AVYQPyVluAgGAgAZBRgQGQYQPxkZBhLJBWW1tgAwuSUFBQlJNQUFBQVltgAIBgAGEBZ3P//////////////////////////xZjFdrL6mDgG4iIiIhgQFFgJAFhBD6Uk5KRkGErv1ZbYEBRYCCBgwMDgVKQYEBSkHv/////////////////////////////////////GRZgIIIBgFF7/////////////////////////////////////4OBgxYXg1JQUFBQYEBRYQSokZBhLEBWW2AAYEBRgIMDgWAAhlrxkVBQPYBgAIEUYQTlV2BAUZFQYB8ZYD89ARaCAWBAUj2CUj1gAGAghAE+YQTqVltgYJFQW1CRUJFQgWEE+1dgFWEFEFZbgIBgIAGQUYEBkGEFD5GQYSyQVltbYAMLklBQUJSTUFBQUFZbYABgBWf//////////4ERFWEFPFdhBTthIvxWW1tgQFGQgIJSgGAgAmAgAYIBYEBSgBVhBXVXgWAgAVthBWJhIHJWW4FSYCABkGABkAOQgWEFWleQUFtQkFBhBYdgAGABYAKJYRbIVluBYACBUYEQYQWbV2EFmmEsvVZbW2AgAmAgAQGBkFJQYQW0YAJgA4CIYRbIVluBYAGBUYEQYQXIV2EFx2EsvVZbW2AgAmAgAQGBkFJQYQXgYARgAYZhFwFWW4FgAoFRgRBhBfRXYQXzYSy9VltbYCACYCABAYGQUlBhBgxgBmABhmEXAVZbgWADgVGBEGEGIFdhBh9hLL1WW1tgIAJgIAEBgZBSUGEGOGAFYASGYRcBVluBYASBUYEQYQZMV2EGS2EsvVZbW2AgAmAgAQGBkFJQYQZfYSCSVltgAIFgAAGQYAcLkIFgBwuBUlBQg4FgIAGQc///////////////////////////FpCBc///////////////////////////FoFSUFCCgWBAAZBgBwuQgWAHC4FSUFBhBsRhIM9WW4iBYEABkHP//////////////////////////xaQgXP//////////////////////////xaBUlBQgoFg4AGBkFJQgYFhAQABgZBSUGAAYQcbi4NhFzhWW5BQYBZgAwuBFGEHY1dgQFF/CMN5oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBUmAEAWEHWpBhLUlWW2BAUYCRA5D9W1BQUFBQUFBQUFBQVlthB3hhIT5WW2AAgGEHhYWFYRhQVluRUGAHC5FQYBZgAwuCFGEHnFdgAID9W4CSUFBQkpFQUFZbYACAYQFnc///////////////////////////FmNhjcZeYOAbhYVgQFFgJAFhB96SkZBhLbNWW2BAUWAggYMDA4FSkGBAUpB7/////////////////////////////////////xkWYCCCAYBRe/////////////////////////////////////+DgYMWF4NSUFBQUGBAUWEISJGQYSxAVltgAGBAUYCDA4FgAIZa8ZFQUD2AYACBFGEIhVdgQFGRUGAfGWA/PQEWggFgQFI9glI9YABgIIQBPmEIilZbYGCRUFtQkVCRUH9K9HgOBv6Mud9ksHlPpvATma+XkXW7mI414OV+WUVnvIKCYEBRYQjAkpGQYS3yVltgQFGAkQOQoVBQUFBWW2AAYAVn//////////+BERVhCOtXYQjqYSL8VltbYEBRkICCUoBgIAJgIAGCAWBAUoAVYQkkV4FgIAFbYQkRYSByVluBUmAgAZBgAZADkIFhCQlXkFBbUJBQYQk2YABgAWACh2EWyFZbgWAAgVGBEGEJSldhCUlhLL1WW1tgIAJgIAEBgZBSUGEJY2ACYAOAhmEWyFZbgWABgVGBEGEJd1dhCXZhLL1WW1tgIAJgIAEBgZBSUGEJj2AEYAGEYRcBVluBYAKBUYEQYQmjV2EJomEsvVZbW2AgAmAgAQGBkFJQYQm7YAZgAYRhFwFWW4FgA4FRgRBhCc9XYQnOYSy9VltbYCACYCABAYGQUlBhCedgBWAEhGEXAVZbgWAEgVGBEGEJ+1dhCfphLL1WW1tgIAJgIAEBgZBSUGAAYQoShoNhGYRWW2AHC5BQYBZgAwuBFGEKXVdgQFF/CMN5oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBUmAEAWEKVJBhLm5WW2BAUYCRA5D9W1BQUFBQUFZbYABgAWf//////////4ERFWEKgldhCoFhIvxWW1tgQFGQgIJSgGAgAmAgAYIBYEBSgBVhCrtXgWAgAVthCqhhIHJWW4FSYCABkGABkAOQgWEKoFeQUFtQkFBhCsZhIHJWW2AEgWAAAYGBUlBQYQrZYSE+VlswgWAgAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUGABgWAAAZAVFZCBFRWBUlBQgIJgIAGBkFJQgYNgAIFRgRBhC0BXYQs/YSy9VltbYCACYCABAYGQUlBgAGELWzBgAIiIiGEanFZbkFBgAGELaYiDYRc4VluQUGAWYAMLgRRhC3tXYACA/VtQUFBQUFBQUFZbYACAYABhAWdz//////////////////////////8WY5sj09lg4BuIiIiIYEBRYCQBYQvClJOSkZBhK79WW2BAUWAggYMDA4FSkGBAUpB7/////////////////////////////////////xkWYCCCAYBRe/////////////////////////////////////+DgYMWF4NSUFBQUGBAUWEMLJGQYSxAVltgAGBAUYCDA4FgAIZa8ZFQUD2AYACBFGEMaVdgQFGRUGAfGWA/PQEWggFgQFI9glI9YABgIIQBPmEMblZbYGCRUFtQkVCRUIFhDH9XYBVhDJRWW4CAYCABkFGBAZBhDJORkGEskFZbW2ADC5JQUFCUk1BQUFBWW2AAYAVn//////////+BERVhDMBXYQy/YSL8VltbYEBRkICCUoBgIAJgIAGCAWBAUoAVYQz5V4FgIAFbYQzmYSByVluBUmAgAZBgAZADkIFhDN5XkFBbUJBQYQ0LYABgAWACjGEWyFZbgWAAgVGBEGENH1dhDR5hLL1WW1tgIAJgIAEBgZBSUGENOGACYAOAi2EWyFZbgWABgVGBEGENTFdhDUthLL1WW1tgIAJgIAEBgZBSUGENZGAEYAGJYRcBVluBYAKBUYEQYQ14V2ENd2EsvVZbW2AgAmAgAQGBkFJQYQ2QYAZgAYlhFwFWW4FgA4FRgRBhDaRXYQ2jYSy9VltbYCACYCABAYGQUlBhDbxgBWAEiWEXAVZbgWAEgVGBEGEN0FdhDc9hLL1WW1tgIAJgIAEBgZBSUINgApCBYQ3qkZBhMKVWW1CCYAOQgWEN+pGQYTClVltQgWAEkIFhDgqRkGEwpVZbUGAAYQ4bi2AAiYmGYRqcVluQUGAAYQ4pjYNhFzhWW5BQYBZgAwuBFGEOcVdgQFF/CMN5oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBUmAEAWEOaJBhLUlWW2BAUYCRA5D9W1BQUFBQUFBQUFBQUFBWW2AAgGAAYQFnc///////////////////////////FmMV2svqYOAbiIiIiGBAUWAkAWEOvZSTkpGQYSu/VltgQFFgIIGDAwOBUpBgQFKQe/////////////////////////////////////8ZFmAgggGAUXv/////////////////////////////////////g4GDFheDUlBQUFBgQFFhDyeRkGEsQFZbYABgQFGAgwOBhVr0kVBQPYBgAIEUYQ9iV2BAUZFQYB8ZYD89ARaCAWBAUj2CUj1gAGAghAE+YQ9nVltgYJFQW1CRUJFQgWEPeFdgFWEPjVZbgIBgIAGQUYEBkGEPjJGQYSyQVltbYAMLklBQUJSTUFBQUFZbYABgBWf//////////4ERFWEPuVdhD7hhIvxWW1tgQFGQgIJSgGAgAmAgAYIBYEBSgBVhD/JXgWAgAVthD99hIHJWW4FSYCABkGABkAOQgWEP11eQUFtQkFBhEARgAGABYAKHYRbIVluBYACBUYEQYRAYV2EQF2EsvVZbW2AgAmAgAQGBkFJQYRAxYAJgA4CGYRbIVluBYAGBUYEQYRBFV2EQRGEsvVZbW2AgAmAgAQGBkFJQYRBdYARgAYRhFwFWW4FgAoFRgRBhEHFXYRBwYSy9VltbYCACYCABAYGQUlBhEIlgBmABhGEXAVZbgWADgVGBEGEQnVdhEJxhLL1WW1tgIAJgIAEBgZBSUGEQtWAFYASEYRcBVluBYASBUYEQYRDJV2EQyGEsvVZbW2AgAmAgAQGBkFJQYRDcYSDPVluFgWBAAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUGACgFRhESGQYS7IVluAYB8BYCCAkQQCYCABYEBRkIEBYEBSgJKRkIGBUmAgAYKAVGERTZBhLshWW4AVYRGaV4BgHxBhEW9XYQEAgINUBAKDUpFgIAGRYRGaVluCAZGQYABSYCBgACCQW4FUgVKQYAEBkGAgAYCDEWERfVeCkANgHxaCAZFbUFBQUFCBYAABgZBSUGADgFRhEbSQYS7IVluAYB8BYCCAkQQCYCABYEBRkIEBYEBSgJKRkIGBUmAgAYKAVGER4JBhLshWW4AVYRItV4BgHxBhEgJXYQEAgINUBAKDUpFgIAGRYRItVluCAZGQYABSYCBgACCQW4FUgVKQYAEBkGAgAYCDEWESEFeCkANgHxaCAZFbUFBQUFCBYCABgZBSUIGBYOABgZBSUGAEgFRhElCQYS7IVluAYB8BYCCAkQQCYCABYEBRkIEBYEBSgJKRkIGBUmAgAYKAVGESfJBhLshWW4AVYRLJV4BgHxBhEp5XYQEAgINUBAKDUpFgIAGRYRLJVluCAZGQYABSYCBgACCQW4FUgVKQYAEBkGAgAYCDEWESrFeCkANgHxaCAZFbUFBQUFCBYGABgZBSUGAAYRLiiINhFzhWW5BQYBZgAwuBFGETKldgQFF/CMN5oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBUmAEAWETIZBhMcNWW2BAUYCRA5D9W1BQUFBQUFBQVlthEzxhIM9WW2ACgFRhE0mQYS7IVluAYB8BYCCAkQQCYCABYEBRkIEBYEBSgJKRkIGBUmAgAYKAVGETdZBhLshWW4AVYRPCV4BgHxBhE5dXYQEAgINUBAKDUpFgIAGRYRPCVluCAZGQYABSYCBgACCQW4FUgVKQYAEBkGAgAYCDEWETpVeCkANgHxaCAZFbUFBQUFCBYAABgZBSUGADgFRhE9yQYS7IVluAYB8BYCCAkQQCYCABYEBRkIEBYEBSgJKRkIGBUmAgAYKAVGEUCJBhLshWW4AVYRRVV4BgHxBhFCpXYQEAgINUBAKDUpFgIAGRYRRVVluCAZGQYABSYCBgACCQW4FUgVKQYAEBkGAgAYCDEWEUOFeCkANgHxaCAZFbUFBQUFCBYCABgZBSUIGBYEABkHP//////////////////////////xaQgXP//////////////////////////xaBUlBQYASAVGEUp5BhLshWW4BgHwFgIICRBAJgIAFgQFGQgQFgQFKAkpGQgYFSYCABgoBUYRTTkGEuyFZbgBVhFSBXgGAfEGEU9VdhAQCAg1QEAoNSkWAgAZFhFSBWW4IBkZBgAFJgIGAAIJBbgVSBUpBgAQGQYCABgIMRYRUDV4KQA2AfFoIBkVtQUFBQUIFgYAGBkFJQYABhFTmEg2EXOFZbkFBgFmADC4EUYRWBV2BAUX8Iw3mgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIFSYAQBYRV4kGEyVVZbYEBRgJEDkP1bUFBQUFZbYRWPYSDPVluCgWAAAYGQUlCBgWAgAYGQUlCDgWBAAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUGAEgFRhFeaQYS7IVluAYB8BYCCAkQQCYCABYEBRkIEBYEBSgJKRkIGBUmAgAYKAVGEWEpBhLshWW4AVYRZfV4BgHxBhFjRXYQEAgINUBAKDUpFgIAGRYRZfVluCAZGQYABSYCBgACCQW4FUgVKQYAEBkGAgAYCDEWEWQleCkANgHxaCAZFbUFBQUFCBYGABgZBSUGAAYRZ4hoNhFzhWW5BQYBZgAwuBFGEWwFdgQFF/CMN5oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBUmAEAWEWt5BhMudWW2BAUYCRA5D9W1BQUFBQUFZbYRbQYSByVltgQFGAYEABYEBSgGEW5YeHYR0WVluBUmAgAWEW9IWFYR1sVluBUlCQUJSTUFBQUFZbYRcJYSByVltgQFGAYEABYEBSgGEXHYZhH0FWW4FSYCABYRcshYVhH4JWW4FSUJBQk5JQUFBWW2AAgGAAYQFnc///////////////////////////FmN9MFz6YOAbhoZgQFFgJAFhF3GSkZBhNgdWW2BAUWAggYMDA4FSkGBAUpB7/////////////////////////////////////xkWYCCCAYBRe/////////////////////////////////////+DgYMWF4NSUFBQUGBAUWEX25GQYSxAVltgAGBAUYCDA4FgAIZa8ZFQUD2AYACBFGEYGFdgQFGRUGAfGWA/PQEWggFgQFI9glI9YABgIIQBPmEYHVZbYGCRUFtQkVCRUIFhGC5XYBVhGENWW4CAYCABkFGBAZBhGEKRkGEskFZbW2ADC5JQUFCSkVBQVltgAGEYWmEhPlZbYACAYQFnc///////////////////////////FmM8TdMuYOAbh4dgQFFgJAFhGJGSkZBhNjdWW2BAUWAggYMDA4FSkGBAUpB7/////////////////////////////////////xkWYCCCAYBRe/////////////////////////////////////+DgYMWF4NSUFBQUGBAUWEY+5GQYSxAVltgAGBAUYCDA4FgAIZa8ZFQUD2AYACBFGEZOFdgQFGRUGAfGWA/PQEWggFgQFI9glI9YABgIIQBPmEZPVZbYGCRUFtQkVCRUGEZSmEhPlZbgmEZV1dgFYFhGWxWW4GAYCABkFGBAZBhGWuRkGE331ZbW4FgAwuRUICVUIGWUFBQUFBQklCSkFBWW2AAgGAAYQFnc///////////////////////////FmNvw8uvYOAbhoZgQFFgJAFhGb2SkZBhOMFWW2BAUWAggYMDA4FSkGBAUpB7/////////////////////////////////////xkWYCCCAYBRe/////////////////////////////////////+DgYMWF4NSUFBQUGBAUWEaJ5GQYSxAVltgAGBAUYCDA4FgAIZa8ZFQUD2AYACBFGEaZFdgQFGRUGAfGWA/PQEWggFgQFI9glI9YABgIIQBPmEaaVZbYGCRUFtQkVCRUIFhGnpXYBVhGo9WW4CAYCABkFGBAZBhGo6RkGEskFZbW2ADC5JQUFCSkVBQVlthGqRhIM9WW2EarGEgklZbhYFgAAGQYAcLkIFgBwuBUlBQhIFgIAGQc///////////////////////////FpCBc///////////////////////////FoFSUFCDgWBAAZBgBwuQgWAHC4FSUFBgAoBUYRsVkGEuyFZbgGAfAWAggJEEAmAgAWBAUZCBAWBAUoCSkZCBgVJgIAGCgFRhG0GQYS7IVluAFWEbjleAYB8QYRtjV2EBAICDVAQCg1KRYCABkWEbjlZbggGRkGAAUmAgYAAgkFuBVIFSkGABAZBgIAGAgxFhG3FXgpADYB8WggGRW1BQUFBQgmAAAYGQUlBgA4BUYRuokGEuyFZbgGAfAWAggJEEAmAgAWBAUZCBAWBAUoCSkZCBgVJgIAGCgFRhG9SQYS7IVluAFWEcIVeAYB8QYRv2V2EBAICDVAQCg1KRYCABkWEcIVZbggGRkGAAUmAgYAAgkFuBVIFSkGABAZBgIAGAgxFhHARXgpADYB8WggGRW1BQUFBQgmAgAYGQUlCGgmBAAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUIKCYOABgZBSUICCYQEAAYGQUlBgBIBUYRyGkGEuyFZbgGAfAWAggJEEAmAgAWBAUZCBAWBAUoCSkZCBgVJgIAGCgFRhHLKQYS7IVluAFWEc/1eAYB8QYRzUV2EBAICDVAQCg1KRYCABkWEc/1ZbggGRkGAAUmAgYAAgkFuBVIFSkGABAZBgIAGAgxFhHOJXgpADYB8WggGRW1BQUFBQgmBgAYGQUlBQlZRQUFBQUFZbYABhHT2DYAaBERVhHS5XYR0tYTjxVltbgmEgXpCRkGP/////FlZbkFBhHWSCYAaBERVhHVVXYR1UYTjxVltbgmEgXpCRkGP/////FlZbkFCSkVBQVlthHXRhIT5WW2AAYASBERVhHYhXYR2HYTjxVltbg2AEgREVYR2bV2EdmmE48VZbWwNhHbZXYAGBYAABkBUVkIEVFYFSUFBhHztWW2ABYASBERVhHcpXYR3JYTjxVltbg2AEgREVYR3dV2Ed3GE48VZbWwNhHj9XYACAVJBhAQAKkARz//////////////////////////8WgWAgAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUGEfOlZbYAJgBIERFWEeU1dhHlJhOPFWW1uDYASBERVhHmZXYR5lYTjxVltbA2EeeVeBgWBAAYGQUlBhHzlWW2ADYASBERVhHo1XYR6MYTjxVltbg2AEgREVYR6gV2Een2E48VZbWwNhHrNXgYFgYAGBkFJQYR84VltgBICBERVhHsZXYR7FYTjxVltbg2AEgREVYR7ZV2Ee2GE48VZbWwNhHzdXYACAVJBhAQAKkARz//////////////////////////8WgWCAAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUFtbW1tbkpFQUFZbYABgAWAAg2AGgREVYR9aV2EfWWE48VZbW2AGgREVYR9sV2Efa2E48VZbW4FSYCABkIFSYCABYAAgVJBQkZBQVlthH4phIT5WW2ABYASBERVhH55XYR+dYTjxVltbg2AEgREVYR+xV2EfsGE48VZbWwNhH/NXgYFgIAGQc///////////////////////////FpCBc///////////////////////////FoFSUFBhIFhWW2AEgIERFWEgBldhIAVhOPFWW1uDYASBERVhIBlXYSAYYTjxVltbA2EgV1eBgWCAAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUFtbkpFQUFZbYACBYP8WYAGQG4MXkFCSkVBQVltgQFGAYEABYEBSgGAAgVJgIAFhIIxhIT5WW4FSUJBWW2BAUYBgYAFgQFKAYABgBwuBUmAgAWAAc///////////////////////////FoFSYCABYABgBwuBUlCQVltgQFGAYQEgAWBAUoBgYIFSYCABYGCBUmAgAWAAc///////////////////////////FoFSYCABYGCBUmAgAWAAFRWBUmAgAWAAYAcLgVJgIAFgABUVgVJgIAFgYIFSYCABYSE4YSCSVluBUlCQVltgQFGAYKABYEBSgGAAFRWBUmAgAWAAc///////////////////////////FoFSYCABYGCBUmAgAWBggVJgIAFgAHP//////////////////////////xaBUlCQVltgAGBAUZBQkFZbYACA/VtgAID9W2AAc///////////////////////////ghaQUJGQUFZbYABhIdqCYSGvVluQUJGQUFZbYSHqgWEhz1ZbgRRhIfVXYACA/VtQVltgAIE1kFBhIgeBYSHhVluSkVBQVltgAIGQUJGQUFZbYSIggWEiDVZbgRRhIitXYACA/VtQVltgAIE1kFBhIj2BYSIXVluSkVBQVltgAIBgAIBggIWHAxIVYSJdV2EiXGEhpVZbW2AAYSJrh4KIAWEh+FZblFBQYCBhInyHgogBYSH4VluTUFBgQGEijYeCiAFhIfhWW5JQUGBgYSKeh4KIAWEiLlZbkVBQkpWRlFCSUFZbYACBYAcLkFCRkFBWW2EiwIFhIqpWW4JSUFBWW2AAYCCCAZBQYSLbYACDAYRhIrdWW5KRUFBWW2AAgP1bYACA/VtgAGAfGWAfgwEWkFCRkFBWW39OSHtxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAUmBBYARSYCRgAP1bYSM0gmEi61ZbgQGBgRBn//////////+CERcVYSNTV2EjUmEi/FZbW4BgQFJQUFBWW2AAYSNmYSGbVluQUGEjcoKCYSMrVluRkFBWW2AAZ///////////ghEVYSOSV2EjkWEi/FZbW2Ejm4JhIutWW5BQYCCBAZBQkZBQVluCgYM3YACDgwFSUFBQVltgAGEjymEjxYRhI3dWW2EjXFZbkFCCgVJgIIEBhISEAREVYSPmV2Ej5WEi5lZbW2Ej8YSChWEjqFZbUJOSUFBQVltgAIJgH4MBEmEkDldhJA1hIuFWW1uBNWEkHoSCYCCGAWEjt1ZbkVBQkpFQUFZbYSQwgWEiqlZbgRRhJDtXYACA/VtQVltgAIE1kFBhJE2BYSQnVluSkVBQVltgAIBgAIBgAIBgAGDgiIoDEhVhJHJXYSRxYSGlVltbYABhJICKgosBYSH4VluXUFBgIGEkkYqCiwFhIfhWW5ZQUGBAiAE1Z///////////gREVYSSyV2EksWEhqlZbW2EkvoqCiwFhI/lWW5VQUGBgiAE1Z///////////gREVYSTfV2Ek3mEhqlZbW2Ek64qCiwFhI/lWW5RQUGCAYST8ioKLAWEh+FZbk1BQYKBhJQ2KgosBYSH4VluSUFBgwGElHoqCiwFhJD5WW5FQUJKVmJGUl1CSlVBWW2AAgGBAg4UDEhVhJURXYSVDYSGlVltbYABhJVKFgoYBYSH4VluSUFBgIGElY4WChgFhIi5WW5FQUJJQkpBQVltgAIEVFZBQkZBQVlthJYKBYSVtVluCUlBQVlthJZGBYSHPVluCUlBQVltgAIFRkFCRkFBWW2AAgoJSYCCCAZBQkpFQUFZbYABbg4EQFWEl0VeAggFRgYQBUmAggQGQUGEltlZbYACEhAFSUFBQUFZbYABhJeiCYSWXVlthJfKBhWElolZbk1BhJgKBhWAghgFhJbNWW2EmC4FhIutWW4QBkVBQkpFQUFZbYABgoIMBYACDAVFhJi5gAIYBgmEleVZbUGAggwFRYSZBYCCGAYJhJYhWW1BgQIMBUYSCA2BAhgFSYSZZgoJhJd1WW5FQUGBggwFRhIIDYGCGAVJhJnOCgmEl3VZbkVBQYICDAVFhJohggIYBgmEliFZbUICRUFCSkVBQVltgAGAgggGQUIGBA2AAgwFSYSatgYRhJhZWW5BQkpFQUFZbYACAYECDhQMSFWEmzFdhJsthIaVWW1tgAGEm2oWChgFhIfhWW5JQUGAggwE1Z///////////gREVYSb7V2Em+mEhqlZbW2EnB4WChgFhI/lWW5FQUJJQkpBQVltgAIBgAIBggIWHAxIVYScrV2EnKmEhpVZbW2AAYSc5h4KIAWEh+FZblFBQYCCFATVn//////////+BERVhJ1pXYSdZYSGqVltbYSdmh4KIAWEj+VZbk1BQYECFATVn//////////+BERVhJ4dXYSeGYSGqVltbYSeTh4KIAWEj+VZbklBQYGBhJ6SHgogBYSH4VluRUFCSlZGUUJJQVltgAIBgAGBghIYDEhVhJ8lXYSfIYSGlVltbYABhJ9eGgocBYSH4VluTUFBgIGEn6IaChwFhIfhWW5JQUGBAYSf5hoKHAWEkPlZbkVBQklCSUJJWW2AAZ///////////ghEVYSgeV2EoHWEi/FZbW2EoJ4JhIutWW5BQYCCBAZBQkZBQVltgAGEoR2EoQoRhKANWW2EjXFZbkFCCgVJgIIEBhISEAREVYShjV2EoYmEi5lZbW2EoboSChWEjqFZbUJOSUFBQVltgAIJgH4MBEmEoi1dhKIphIuFWW1uBNWEom4SCYCCGAWEoNFZbkVBQkpFQUFZbYACAYACAYACAYACAYACAYQFAi40DEhVhKMhXYSjHYSGlVltbYABhKNaNgo4BYSH4VluaUFBgIGEo542CjgFhIfhWW5lQUGBAiwE1Z///////////gREVYSkIV2EpB2EhqlZbW2EpFI2CjgFhI/lWW5hQUGBgiwE1Z///////////gREVYSk1V2EpNGEhqlZbW2EpQY2CjgFhI/lWW5dQUGCAYSlSjYKOAWEh+FZbllBQYKBhKWONgo4BYSH4VluVUFBgwGEpdI2CjgFhJD5WW5RQUGDgiwE1Z///////////gREVYSmVV2EplGEhqlZbW2EpoY2CjgFhKHZWW5NQUGEBAIsBNWf//////////4ERFWEpw1dhKcJhIapWW1thKc+Ngo4BYSh2VluSUFBhASCLATVn//////////+BERVhKfFXYSnwYSGqVltbYSn9jYKOAWEodlZbkVBQkpWYm5GUl5pQkpWYUFZbYACAYACAYABgoIaIAxIVYSorV2EqKmEhpVZbW2AAYSo5iIKJAWEh+FZblVBQYCBhKkqIgokBYSH4VluUUFBgQIYBNWf//////////4ERFWEqa1dhKmphIapWW1thKneIgokBYSP5VluTUFBgYIYBNWf//////////4ERFWEqmFdhKpdhIapWW1thKqSIgokBYSP5VluSUFBggGEqtYiCiQFhIfhWW5FQUJKVUJKVkJNQVltgAIBgQIOFAxIVYSrZV2Eq2GEhpVZbW2AAYSrnhYKGAWEh+FZbklBQYCBhKviFgoYBYSH4VluRUFCSUJKQUFZbYACAYACAYICFhwMSFWErHFdhKxthIaVWW1tgAGErKoeCiAFhIfhWW5RQUGAgYSs7h4KIAWEh+FZbk1BQYECFATVn//////////+BERVhK1xXYStbYSGqVltbYStoh4KIAWEodlZbklBQYGCFATVn//////////+BERVhK4lXYSuIYSGqVltbYSuVh4KIAWEodlZbkVBQkpWRlFCSUFZbYSuqgWEhz1ZbglJQUFZbYSu5gWEiDVZbglJQUFZbYABggIIBkFBhK9RgAIMBh2EroVZbYSvhYCCDAYZhK6FWW2Er7mBAgwGFYSuhVlthK/tgYIMBhGErsFZblZRQUFBQUFZbYACBkFCSkVBQVltgAGEsGoJhJZdWW2EsJIGFYSwEVluTUGEsNIGFYCCGAWEls1ZbgIQBkVBQkpFQUFZbYABhLEyChGEsD1ZbkVCBkFCSkVBQVltgAIFgAwuQUJGQUFZbYSxtgWEsV1ZbgRRhLHhXYACA/VtQVltgAIFRkFBhLIqBYSxkVluSkVBQVltgAGAggoQDEhVhLKZXYSylYSGlVltbYABhLLSEgoUBYSx7VluRUFCSkVBQVlt/Tkh7cQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAFJgMmAEUmAkYAD9W2AAgoJSYCCCAZBQkpFQUFZbf1VwZGF0ZSBvZiB0b2tlbkluZm8gZmFpbGVkIQAAAAAAYACCAVJQVltgAGEtM2Abg2Es7FZbkVBhLT6CYSz9VltgIIIBkFCRkFBWW2AAYCCCAZBQgYEDYACDAVJhLWKBYS0mVluQUJGQUFZbYACCglJgIIIBkFCSkVBQVltgAGEthYJhJZdWW2Etj4GFYS1pVluTUGEtn4GFYCCGAWEls1ZbYS2ogWEi61ZbhAGRUFCSkVBQVltgAGBAggGQUGEtyGAAgwGFYSuhVluBgQNgIIMBUmEt2oGEYS16VluQUJOSUFBQVlthLeyBYSVtVluCUlBQVltgAGBAggGQUGEuB2AAgwGFYS3jVluBgQNgIIMBUmEuGYGEYS16VluQUJOSUFBQVlt/VXBkYXRlIG9mIHRva2VuIGtleXMgZmFpbGVkIQAAAABgAIIBUlBWW2AAYS5YYByDYSzsVluRUGEuY4JhLiJWW2AgggGQUJGQUFZbYABgIIIBkFCBgQNgAIMBUmEuh4FhLktWW5BQkZBQVltgAIFRkFCRkFBWW39OSHtxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAUmAiYARSYCRgAP1bYABgAoIEkFBgAYIWgGEu4Fdgf4IWkVBbYCCCEIEDYS7zV2Eu8mEumVZbW1CRkFBWW2AAgZBQgWAAUmAgYAAgkFCRkFBWW2AAYCBgH4MBBJBQkZBQVltgAIKCG5BQkpFQUFZbYABgCIMCYS9bf///////////////////////////////////////////gmEvHlZbYS9lhoNhLx5WW5VQgBmEFpNQgIYWhBeSUFBQk5JQUFBWW2AAgZBQkZBQVltgAGEvomEvnWEvmIRhIg1WW2EvfVZbYSINVluQUJGQUFZbYACBkFCRkFBWW2EvvINhL4dWW2Ev0GEvyIJhL6lWW4SEVGEvK1ZbglVQUFBQVltgAJBWW2Ev5WEv2FZbYS/wgYSEYS+zVltQUFBWW1uBgRAVYTAUV2EwCWAAgmEv3VZbYAGBAZBQYS/2VltQUFZbYB+CERVhMFlXYTAqgWEu+VZbYTAzhGEvDlZbgQFgIIUQFWEwQleBkFBbYTBWYTBOhWEvDlZbgwGCYS/1VltQUFtQUFBWW2AAgoIckFCSkVBQVltgAGEwfGAAGYRgCAJhMF5WWxmAgxaRUFCSkVBQVltgAGEwlYODYTBrVluRUIJgAgKCF5BQkpFQUFZbYTCugmEujlZbZ///////////gREVYTDHV2EwxmEi/FZbW2Ew0YJUYS7IVlthMNyCgoVhMBhWW2AAYCCQUGAfgxFgAYEUYTEPV2AAhBVhMP1XgocBUZBQW2ExB4WCYTCJVluGVVBhMW9WW2AfGYQWYTEdhmEu+VZbYABbgoEQFWExRVeEiQFRglVgAYIBkVBgIIUBlFBgIIEBkFBhMSBWW4aDEBVhMWJXhIkBUWExXmAfiRaCYTBrVluDVVBbYAFgAogCAYhVUFBQW1BQUFBQUFZbf1VwZGF0ZSBvZiB0b2tlbkluZm8ga2V5cyBmYWlsZWQhYACCAVJQVltgAGExrWAgg2Es7FZbkVBhMbiCYTF3VltgIIIBkFCRkFBWW2AAYCCCAZBQgYEDYACDAVJhMdyBYTGgVluQUJGQUFZbf1VwZGF0ZSBvZiB0b2tlbkluZm8udHJlYXN1cnkgZmFpYACCAVJ/bGVkIQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgIIIBUlBWW2AAYTI/YCSDYSzsVluRUGEySoJhMeNWW2BAggGQUJGQUFZbYABgIIIBkFCBgQNgAIMBUmEyboFhMjJWW5BQkZBQVlt/VXBkYXRlIG9mIHRva2VuSW5mbyBuYW1lIGFuZCBzeW1gAIIBUn9ib2wgZmFpbGVkIQAAAAAAAAAAAAAAAAAAAAAAAAAAAGAgggFSUFZbYABhMtFgK4NhLOxWW5FQYTLcgmEydVZbYECCAZBQkZBQVltgAGAgggGQUIGBA2AAgwFSYTMAgWEyxFZbkFCRkFBWW2AAgoJSYCCCAZBQkpFQUFZbYABhMyOCYS6OVlthMy2BhWEzB1Zbk1BhMz2BhWAghgFhJbNWW2EzRoFhIutWW4QBkVBQkpFQUFZbYTNagWEiqlZbglJQUFZbYACBUZBQkZBQVltgAIKCUmAgggGQUJKRUFBWW2AAgZBQYCCCAZBQkZBQVlthM5WBYSINVluCUlBQVltgAGCggwFgAIMBUWEzs2AAhgGCYSV5VltQYCCDAVFhM8ZgIIYBgmEliFZbUGBAgwFRhIIDYECGAVJhM96CgmEl3VZbkVBQYGCDAVGEggNgYIYBUmEz+IKCYSXdVluRUFBggIMBUWE0DWCAhgGCYSWIVltQgJFQUJKRUFBWW2AAYECDAWAAgwFRYTQwYACGAYJhM4xWW1BgIIMBUYSCA2AghgFSYTRIgoJhM5tWW5FQUICRUFCSkVBQVltgAGE0YYODYTQYVluQUJKRUFBWW2AAYCCCAZBQkZBQVltgAGE0gYJhM2BWW2E0i4GFYTNrVluTUINgIIIChQFhNJ2FYTN8VluAYABbhYEQFWE02VeEhAOJUoFRYTS6hYJhNFVWW5RQYTTFg2E0aVZbklBgIIoBmVBQYAGBAZBQYTShVltQgpdQh5VQUFBQUFCSkVBQVltgYIIBYACCAVFhNQFgAIUBgmEzUVZbUGAgggFRYTUUYCCFAYJhJYhWW1BgQIIBUWE1J2BAhQGCYTNRVltQUFBQVltgAGEBYIMBYACDAVGEggNgAIYBUmE1S4KCYTMYVluRUFBgIIMBUYSCA2AghgFSYTVlgoJhMxhWW5FQUGBAgwFRYTV6YECGAYJhJYhWW1BgYIMBUYSCA2BghgFSYTWSgoJhMxhWW5FQUGCAgwFRYTWnYICGAYJhJXlWW1BgoIMBUWE1umCghgGCYTNRVltQYMCDAVFhNc1gwIYBgmEleVZbUGDggwFRhIIDYOCGAVJhNeWCgmE0dlZbkVBQYQEAgwFRYTX8YQEAhgGCYTTrVltQgJFQUJKRUFBWW2AAYECCAZBQYTYcYACDAYVhK6FWW4GBA2AggwFSYTYugYRhNS1WW5BQk5JQUFBWW2AAYECCAZBQYTZMYACDAYVhK6FWW2E2WWAggwGEYSuwVluTklBQUFZbYACA/VtgAID9W2E2c4FhJW1WW4EUYTZ+V2AAgP1bUFZbYACBUZBQYTaQgWE2alZbkpFQUFZbYACBUZBQYTalgWEh4VZbkpFQUFZbYABhNr5hNrmEYSN3VlthI1xWW5BQgoFSYCCBAYSEhAERFWE22ldhNtlhIuZWW1thNuWEgoVhJbNWW1CTklBQUFZbYACCYB+DARJhNwJXYTcBYSLhVltbgVFhNxKEgmAghgFhNqtWW5FQUJKRUFBWW2AAYKCChAMSFWE3MVdhNzBhNmBWW1thNztgoGEjXFZbkFBgAGE3S4SChQFhNoFWW2AAgwFSUGAgYTdfhIKFAWE2llZbYCCDAVJQYECCAVFn//////////+BERVhN4NXYTeCYTZlVltbYTePhIKFAWE27VZbYECDAVJQYGCCAVFn//////////+BERVhN7NXYTeyYTZlVltbYTe/hIKFAWE27VZbYGCDAVJQYIBhN9OEgoUBYTaWVltggIMBUlCSkVBQVltgAIBgQIOFAxIVYTf2V2E39WEhpVZbW2AAYTgEhYKGAWEse1ZbklBQYCCDAVFn//////////+BERVhOCVXYTgkYSGqVltbYTgxhYKGAWE3G1ZbkVBQklCSkFBWW2AAgoJSYCCCAZBQkpFQUFZbYABhOFeCYTNgVlthOGGBhWE4O1Zbk1CDYCCCAoUBYThzhWEzfFZbgGAAW4WBEBVhOK9XhIQDiVKBUWE4kIWCYTRVVluUUGE4m4NhNGlWW5JQYCCKAZlQUGABgQGQUGE4d1ZbUIKXUIeVUFBQUFBQkpFQUFZbYABgQIIBkFBhONZgAIMBhWEroVZbgYEDYCCDAVJhOOiBhGE4TFZbkFCTklBQUFZbf05Ie3EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYABSYCFgBFJgJGAA/f6iZGlwZnNYIhIg7QFwC+WSFuwDFluKIf2VzM+UdsVrk6tZl4cqjf3sumRkc29sY0MACBIAMyKAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAogKjDAToDGPUHShYKFAAAAAAAAAAAAAAAAAAAAAAAAAP1cgcKAxj1BxABUhgKCgoCGAIQ/6/W2AEKCgoCGGIQgLDW2AE="},{"b64Body":"Cg8KCQjH4J6tBhDfBxICGAISAhgDGOLSn/kFIgIIeDIgw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo7qAYICCgZUb2tlbkQSCEtGSFNDQ01TKgMY8Qc6IhIgZHy/1vpgGCrQrgGLkFRwq0JNC7N0uRbiofdh1aER9Z9CIhIgZHy/1vpgGCrQrgGLkFRwq0JNC7N0uRbiofdh1aER9Z9KIhIgZHy/1vpgGCrQrgGLkFRwq0JNC7N0uRbiofdh1aER9Z9SIhIgZHy/1vpgGCrQrgGLkFRwq0JNC7N0uRbiofdh1aER9Z9qDAiDr/mwBhCogeeMAYgBAaIBIhIgZHy/1vpgGCrQrgGLkFRwq0JNC7N0uRbiofdh1aER9Z+yASISIGR8v9b6YBgq0K4Bi5BUcKtCTQuzdLkW4qH3YdWhEfWf","b64Record":"CiUIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkD1IDGPYHEjDvcFTETsIvLoTOUV5FsA0N8/d1Jfuv/GSqc3hxFnhjeYYRhBk74PtOjQkC7Z2zQIQaDAiD4Z6tBhC7maKpASIPCgkIx+CerQYQ3wcSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIAcgoKAxj2BxIDGPEH"},{"b64Body":"Cg8KCQjH4J6tBhDlBxICGAISAhgDGNPtlwgiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjqoCCwoDGPYHGgRuZnQ0","b64Record":"CiUIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkD1gBcgEBEjDePJsqQaqvpzCSvOfloiLNFv03XWxiq1E95VJD9zJqtjyY6rxaBqI5BzltrTVupQ0aDAiD4Z6tBhCb4OWrAyIPCgkIx+CerQYQ5QcSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIAWhIKAxj2BxoLCgIYABIDGPEHGAE="},{"b64Body":"Cg8KCQjI4J6tBhDtBxICGAISAhgDGOC2iyAiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjsICCgoDGPIHEgMY9gc=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwtc8kLyKndMu8KmDFT7oxpRTBD7Se5RsY/6jmkOjpEv61miB+X8+zUoxA1pur2XeUGgwIhOGerQYQ47S8swEiDwoJCMjgnq0GEO0HEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"ChAKCQjI4J6tBhDvBxIDGPIHEgIYAxiA/rWHASICCHgyIMODwq7DgsK3w4PCuXRGOMOCwq5Kw4PCi8ODwpDDg8KOOrcCCgMY9QcQgJL0ARiAqNa5ByKkAnj+cqEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAg8Ppq2b4GM98dAEAAN+ZlR+q52GROzRhZMU5MGqjiSuQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQJpLPPfACaB3BjOwUbKP9yiNO3fNp5Jot0rIq4cmWVI1gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","b64Record":"CiUIISIDGPUHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjAGMXlLJCO+XO0MXEUj9G08SjkRmFrevu2jN/j0DnQGhQAyGBy/oAUDrw2rGNCHTFAaDAiE4Z6tBhCDuMu5AyIQCgkIyOCerQYQ7wcSAxjyByogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo4wgJirbDrSARrKATB4MDhjMzc5YTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxYzU1NzA2NDYxNzQ2NTIwNmY2NjIwNzQ2ZjZiNjU2ZTIwNmI2NTc5NzMyMDY2NjE2OTZjNjU2NDIxMDAwMDAwMDAogKjDAVIZCgoKAhhiEICw1tgBCgsKAxjyBxD/r9bYAQ=="},{"b64Body":"ChIKCQjI4J6tBhDvBxIDGPIHIAGiAqkBCgAqIhIg8Ppq2b4GM98dAEAAN+ZlR+q52GROzRhZMU5MGqjiSuQyIhIg8Ppq2b4GM98dAEAAN+ZlR+q52GROzRhZMU5MGqjiSuQ6IzohAmks898AJoHcGM7BRso/3KI07d82nkmi3SsirhyZZUjWQiM6IQJpLPPfACaB3BjOwUbKP9yiNO3fNp5Jot0rIq4cmWVI1koFCgMY9QdyBUIDGPUHegUKAxj1Bw==","b64Record":"CgMIpwESMO1ci4U5BZb8n7uXF+cwhZNxWbC8IZUc6pfxoENFPOebS4ePjWbvH17Rp2n6kuVIYhoMCIThnq0GEIS4y7kDIhIKCQjI4J6tBhDvBxIDGPIHIAE6zQ4KAxjnAhIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKcaEElOVkFMSURfVE9LRU5fSUQohW5Q54PtAWKEDm/Dy68AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIPD6atm+BjPfHQBAADfmZUfqudhkTs0YWTFOTBqo4krkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACECaSzz3wAmgdwYzsFGyj/cojTt3zaeSaLdKyKuHJllSNYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAagMY9QdSAHoMCIThnq0GEIO4y7kD"},{"b64Body":"ChAKCQjJ4J6tBhDxBxIDGPIHEgIYAxiA/rWHASICCHgyIMODwq7DgsK3w4PCuXRGOMOCwq5Kw4PCi8ODwpDDg8KOOrcCCgMY9QcQgJL0ARiAqNa5ByKkAnj+cqEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD9gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAg8Ppq2b4GM98dAEAAN+ZlR+q52GROzRhZMU5MGqjiSuQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQJpLPPfACaB3BjOwUbKP9yiNO3fNp5Jot0rIq4cmWVI1gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","b64Record":"CiUIISIDGPUHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjBQK4aTDdtfEkzsr0kFvuXKuUCbfdcAH4KGyBdU5kmau514vWVYQZFgWrWUkMLUI8IaDAiF4Z6tBhD7zMviASIQCgkIyeCerQYQ8QcSAxjyByogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo4wgJirbDrSARrKATB4MDhjMzc5YTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxYzU1NzA2NDYxNzQ2NTIwNmY2NjIwNzQ2ZjZiNjU2ZTIwNmI2NTc5NzMyMDY2NjE2OTZjNjU2NDIxMDAwMDAwMDAogKjDAVIZCgoKAhhiEICw1tgBCgsKAxjyBxD/r9bYAQ=="},{"b64Body":"ChIKCQjJ4J6tBhDxBxIDGPIHIAGiAqwBCgMY9gcqIhIg8Ppq2b4GM98dAEAAN+ZlR+q52GROzRhZMU5MGqjiSuQyIhIg8Ppq2b4GM98dAEAAN+ZlR+q52GROzRhZMU5MGqjiSuQ6IzohAmks898AJoHcGM7BRso/3KI07d82nkmi3SsirhyZZUjWQiM6IQJpLPPfACaB3BjOwUbKP9yiNO3fNp5Jot0rIq4cmWVI1koFCgMY9QdyBUIDGPUHegUKAxj1Bw==","b64Record":"CgMIwQESMI4Ldw/17ylKenoWNW8/AMnq2Ftx/V3ZH5OQoGuirKhI3kPwJnW2RMCn5duCwJjGwBoMCIXhnq0GEPzMy+IBIhIKCQjJ4J6tBhDxBxIDGPIHIAE6zw4KAxjnAhIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMEaElRPS0VOX0lTX0lNTVVUQUJMRSiFblDPg+0BYoQOb8PLrwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAg8Ppq2b4GM98dAEAAN+ZlR+q52GROzRhZMU5MGqjiSuQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQJpLPPfACaB3BjOwUbKP9yiNO3fNp5Jot0rIq4cmWVI1gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqAxj1B1IAegwIheGerQYQ+8zL4gE="}]},"getTokenKeyForNonFungibleNegative":{"placeholderNum":1015,"encodedItems":[{"b64Body":"Cg8KCQjN4J6tBhCRCBICGAISAhgDGPuV9hQiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjloxCiISIMbqALPpnZpSVsOX7Ez1GVYoj9n7p1sizFtI82JKArT+EICU69wDSgUIgM7aAw==","b64Record":"CiUIFhIDGPgHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjAoBDH+VLI6zgaYTmVwMqVYxO6At88X/zjoxguCHkn4NcBI/Ecu+a3jbmgrIg1NW0kaCwiK4Z6tBhC7k6UCIg8KCQjN4J6tBhCRCBICGAIqIMODwq7DgsK3w4PCuXRGOMOCwq5Kw4PCi8ODwpDDg8KOUhkKCgoCGAIQ/6fWuQcKCwoDGPgHEICo1rkH"},{"b64Body":"Cg8KCQjO4J6tBhCTCBICGAISAhgDGPuV9hQiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlozCiISILzuZdaTmTc9MKIRqUVTUqDolZTaZc4bSBpXfZp5z6r4EICA6YOx3hZKBQiAztoD","b64Record":"CiUIFhIDGPkHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjAjLJWYHbYxJWdBldBWDN+r0nNxAHGwFf+8g3/TSNxB+By3hTpK2sw+XDWZ87KPZMEaDAiK4Z6tBhD76tuDAiIPCgkIzuCerQYQkwgSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIdCgwKAhgCEP//0YfivC0KDQoDGPkHEICA0ofivC0="},{"b64Body":"Cg8KCQjP4J6tBhCVCBICGAISAhgDGP7LzSwiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjooBjwESDAiKr/mwBhCwoY7cAxptCiISIDr6c3HstD3FiLbfZLQty2FbtSD0HhoL2Kc7s0uqii2TCiM6IQMqrPKW3y+B/prus+a07230jMBClG6ZpID+LTr4vuEqTAoiEiDO+aTTKUNzIct+aurPD6MsyI5GmQkaz0I4wztcJOGdLiIMSGVsbG8gV29ybGQhKgAyAA==","b64Record":"CiUIFhoDGPoHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjB6bDs6GIIormrrCnzBLPnafVrHHOX/JPgPJO0dWksDI92ZejfERAGDrjAC7w9UEFMaCwiL4Z6tBhDDzdcLIg8KCQjP4J6tBhCVCBICGAIqIMODwq7DgsK3w4PCuXRGOMOCwq5Kw4PCi8ODwpDDg8KOUgA="},{"b64Body":"Cg8KCQjP4J6tBhCZCBICGAISAhgDGIudjj4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjpoBiCAKAxj6ByKAIDYwODA2MDQwNTI2MDQwNTE4MDYwNDAwMTYwNDA1MjgwNjAwOTgxNTI2MDIwMDE3Zjc0NmY2YjY1NmU0ZTYxNmQ2NTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA4MTUyNTA2MDAyOTA4MTYyMDAwMDRhOTE5MDYyMDAwNTQwNTY1YjUwNjA0MDUxODA2MDQwMDE2MDQwNTI4MDYwMGI4MTUyNjAyMDAxN2Y3NDZmNmI2NTZlNTM3OTZkNjI2ZjZjMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwODE1MjUwNjAwMzkwODE2MjAwMDA5MTkxOTA2MjAwMDU0MDU2NWI1MDYwNDA1MTgwNjA0MDAxNjA0MDUyODA2MDA0ODE1MjYwMjAwMTdmNmQ2NTZkNmYwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgxNTI1MDYwMDQ5MDgxNjIwMDAwZDg5MTkwNjIwMDA1NDA1NjViNTAzNDgwMTU2MjAwMDBlNjU3NjAwMDgwZmQ1YjUwNjAwMTgwNjAwMDgwNjAwNjgxMTExNTYyMDAwMTAyNTc2MjAwMDEwMTYyMDAwNjI3NTY1YjViNjAwNjgxMTExNTYyMDAwMTE3NTc2MjAwMDExNjYyMDAwNjI3NTY1YjViODE1MjYwMjAwMTkwODE1MjYwMjAwMTYwMDAyMDgxOTA1NTUwNjAwMjYwMDE2MDAwNjAwMTYwMDY4MTExMTU2MjAwMDE0NjU3NjIwMDAxNDU2MjAwMDYyNzU2NWI1YjYwMDY4MTExMTU2MjAwMDE1YjU3NjIwMDAxNWE2MjAwMDYyNzU2NWI1YjgxNTI2MDIwMDE5MDgxNTI2MDIwMDE2MDAwMjA4MTkwNTU1MDYwMDQ2MDAxNjAwMDYwMDI2MDA2ODExMTE1NjIwMDAxOGE1NzYyMDAwMTg5NjIwMDA2Mjc1NjViNWI2MDA2ODExMTE1NjIwMDAxOWY1NzYyMDAwMTllNjIwMDA2Mjc1NjViNWI4MTUyNjAyMDAxOTA4MTUyNjAyMDAxNjAwMDIwODE5MDU1NTA2MDA4NjAwMTYwMDA2MDAzNjAwNjgxMTExNTYyMDAwMWNlNTc2MjAwMDFjZDYyMDAwNjI3NTY1YjViNjAwNjgxMTExNTYyMDAwMWUzNTc2MjAwMDFlMjYyMDAwNjI3NTY1YjViODE1MjYwMjAwMTkwODE1MjYwMjAwMTYwMDAyMDgxOTA1NTUwNjAxMDYwMDE2MDAwNjAwNDYwMDY4MTExMTU2MjAwMDIxMjU3NjIwMDAyMTE2MjAwMDYyNzU2NWI1YjYwMDY4MTExMTU2MjAwMDIyNzU3NjIwMDAyMjY2MjAwMDYyNzU2NWI1YjgxNTI2MDIwMDE5MDgxNTI2MDIwMDE2MDAwMjA4MTkwNTU1MDYwMjA2MDAxNjAwMDYwMDU2MDA2ODExMTE1NjIwMDAyNTY1NzYyMDAwMjU1NjIwMDA2Mjc1NjViNWI2MDA2ODExMTE1NjIwMDAyNmI1NzYyMDAwMjZhNjIwMDA2Mjc1NjViNWI4MTUyNjAyMDAxOTA4MTUyNjAyMDAxNjAwMDIwODE5MDU1NTA2MDQwNjAwMTYwMDA2MDA2ODA4MTExMTU2MjAwMDI5OTU3NjIwMDAyOTg2MjAwMDYyNzU2NWI1YjYwMDY4MTExMTU2MjAwMDJhZTU3NjIwMDAyYWQ2MjAwMDYyNzU2NWI1YjgxNTI2MDIwMDE5MDgxNTI2MDIwMDE2MDAwMjA4MTkwNTU1MDYyMDAwNjU2NTY1YjYwMDA4MTUxOTA1MDkxOTA1MDU2NWI3ZjRlNDg3YjcxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA2MDAwNTI2MDQxNjAwNDUyNjAyNDYwMDBmZDViN2Y0ZTQ4N2I3MTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDUyNjAyMjYwMDQ1MjYwMjQ2MDAwZmQ1YjYwMDA2MDAyODIwNDkwNTA2MDAxODIxNjgwNjIwMDAzNDg1NzYwN2Y4MjE2OTE1MDViNjAyMDgyMTA4MTAzNjIwMDAzNWU1NzYyMDAwMzVkNjIwMDAzMDA1NjViNWI1MDkxOTA1MDU2NWI2MDAwODE5MDUwODE2MDAwNTI2MDIwNjAwMDIwOTA1MDkxOTA1MDU2NWI2MDAwNjAyMDYwMWY4MzAxMDQ5MDUwOTE5MDUwNTY1YjYwMDA4MjgyMWI5MDUwOTI5MTUwNTA1NjViNjAwMDYwMDg4MzAyNjIwMDAzYzg3ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmY4MjYyMDAwMzg5NTY1YjYyMDAwM2Q0ODY4MzYyMDAwMzg5NTY1Yjk1NTA4MDE5ODQxNjkzNTA4MDg2MTY4NDE3OTI1MDUwNTA5MzkyNTA1MDUwNTY1YjYwMDA4MTkwNTA5MTkwNTA1NjViNjAwMDgxOTA1MDkxOTA1MDU2NWI2MDAwNjIwMDA0MjE2MjAwMDQxYjYyMDAwNDE1ODQ2MjAwMDNlYzU2NWI2MjAwMDNmNjU2NWI2MjAwMDNlYzU2NWI5MDUwOTE5MDUwNTY1YjYwMDA4MTkwNTA5MTkwNTA1NjViNjIwMDA0M2Q4MzYyMDAwNDAwNTY1YjYyMDAwNDU1NjIwMDA0NGM4MjYyMDAwNDI4NTY1Yjg0ODQ1NDYyMDAwMzk2NTY1YjgyNTU1MDUwNTA1MDU2NWI2MDAwOTA1NjViNjIwMDA0NmM2MjAwMDQ1ZDU2NWI2MjAwMDQ3OTgxODQ4NDYyMDAwNDMyNTY1YjUwNTA1MDU2NWI1YjgxODExMDE1NjIwMDA0YTE1NzYyMDAwNDk1NjAwMDgyNjIwMDA0NjI1NjViNjAwMTgxMDE5MDUwNjIwMDA0N2Y1NjViNTA1MDU2NWI2MDFmODIxMTE1NjIwMDA0ZjA1NzYyMDAwNGJhODE2MjAwMDM2NDU2NWI2MjAwMDRjNTg0NjIwMDAzNzk1NjViODEwMTYwMjA4NTEwMTU2MjAwMDRkNTU3ODE5MDUwNWI2MjAwMDRlZDYyMDAwNGU0ODU2MjAwMDM3OTU2NWI4MzAxODI2MjAwMDQ3ZTU2NWI1MDUwNWI1MDUwNTA1NjViNjAwMDgyODIxYzkwNTA5MjkxNTA1MDU2NWI2MDAwNjIwMDA1MTU2MDAwMTk4NDYwMDgwMjYyMDAwNGY1NTY1YjE5ODA4MzE2OTE1MDUwOTI5MTUwNTA1NjViNjAwMDYyMDAwNTMwODM4MzYyMDAwNTAyNTY1YjkxNTA4MjYwMDIwMjgyMTc5MDUwOTI5MTUwNTA1NjViNjIwMDA1NGI4MjYyMDAwMmM2NTY1YjY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYyMDAwNTY3NTc2MjAwMDU2NjYyMDAwMmQxNTY1YjViNjIwMDA1NzM4MjU0NjIwMDAzMmY1NjViNjIwMDA1ODA4MjgyODU2MjAwMDRhNTU2NWI2MDAwNjAyMDkwNTA2MDFmODMxMTYwMDE4MTE0NjIwMDA1Yjg1NzYwMDA4NDE1NjIwMDA1YTM1NzgyODcwMTUxOTA1MDViNjIwMDA1YWY4NTgyNjIwMDA1MjI1NjViODY1NTUwNjIwMDA2MWY1NjViNjAxZjE5ODQxNjYyMDAwNWM4ODY2MjAwMDM2NDU2NWI2MDAwNWI4MjgxMTAxNTYyMDAwNWYyNTc4NDg5MDE1MTgyNTU2MDAxODIwMTkxNTA2MDIwODUwMTk0NTA2MDIwODEwMTkwNTA2MjAwMDVjYjU2NWI4NjgzMTAxNTYyMDAwNjEyNTc4NDg5MDE1MTYyMDAwNjBlNjAxZjg5MTY4MjYyMDAwNTAyNTY1YjgzNTU1MDViNjAwMTYwMDI4ODAyMDE4ODU1NTA1MDUwNWI1MDUwNTA1MDUwNTA1NjViN2Y0ZTQ4N2I3MTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDUyNjAyMTYwMDQ1MjYwMjQ2MDAwZmQ1YjYxMzk1NjgwNjIwMDA2NjY2MDAwMzk2MDAwZjNmZTYwODA2MDQwNTI2MDA0MzYxMDYxMDBjMjU3NjAwMDM1NjBlMDFjODA2MzdkY2JiYzcyMTE2MTAwN2Y1NzgwNjNlYWM2ZjNmZTExNjEwMDU5NTc4MDYzZWFjNmYzZmUxNDYxMDI1NDU3ODA2M2ViNTQ4ZWZmMTQ2MTAyOTE1NzgwNjNmMDA3Mjk5MDE0NjEwMmFkNTc4MDYzZjE3MzA3NjAxNDYxMDJjOTU3NjEwMGMyNTY1YjgwNjM3ZGNiYmM3MjE0NjEwMWRmNTc4MDYzOWIyM2QzZDkxNDYxMDFmYjU3ODA2M2U2MDhlMThkMTQ2MTAyMzg1NzYxMDBjMjU2NWI4MDYzMTFlMWZjMDcxNDYxMDBjNzU3ODA2MzE1ZGFjYmVhMTQ2MTAxMDQ1NzgwNjMzYWExMmVmNTE0NjEwMTQxNTc4MDYzNDEyOWI3ZGIxNDYxMDE1ZDU3ODA2MzYxOGRjNjVlMTQ2MTAxOWE1NzgwNjM3OGZlNzJhMTE0NjEwMWMzNTc1YjYwMDA4MGZkNWIzNDgwMTU2MTAwZDM1NzYwMDA4MGZkNWI1MDYxMDBlZTYwMDQ4MDM2MDM4MTAxOTA2MTAwZTk5MTkwNjEyMjQzNTY1YjYxMDJlNTU2NWI2MDQwNTE2MTAwZmI5MTkwNjEyMmM2NTY1YjYwNDA1MTgwOTEwMzkwZjM1YjM0ODAxNTYxMDExMDU3NjAwMDgwZmQ1YjUwNjEwMTJiNjAwNDgwMzYwMzgxMDE5MDYxMDEyNjkxOTA2MTIyNDM1NjViNjEwNDAxNTY1YjYwNDA1MTYxMDEzODkxOTA2MTIyYzY1NjViNjA0MDUxODA5MTAzOTBmMzViNjEwMTViNjAwNDgwMzYwMzgxMDE5MDYxMDE1NjkxOTA2MTI0NTM1NjViNjEwNTFmNTY1YjAwNWIzNDgwMTU2MTAxNjk1NzYwMDA4MGZkNWI1MDYxMDE4NDYwMDQ4MDM2MDM4MTAxOTA2MTAxN2Y5MTkwNjEyNTJkNTY1YjYxMDc3MDU2NWI2MDQwNTE2MTAxOTE5MTkwNjEyNjkzNTY1YjYwNDA1MTgwOTEwMzkwZjM=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwKVOPeQbmez9Tc2qhLdVVGrqptIKZRRWEV+KWA4GBabpR2ikkns2+fJmqCUX/rd20GgwIi+GerQYQi+mujQIiDwoJCM/gnq0GEJkIEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQjQ4J6tBhCfCBICGAISAhgDGPfR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxj6ByKAIDViMzQ4MDE1NjEwMWE2NTc2MDAwODBmZDViNTA2MTAxYzE2MDA0ODAzNjAzODEwMTkwNjEwMWJjOTE5MDYxMjZiNTU2NWI2MTA3YTc1NjViMDA1YjYxMDFkZDYwMDQ4MDM2MDM4MTAxOTA2MTAxZDg5MTkwNjEyNzExNTY1YjYxMDhjZTU2NWIwMDViNjEwMWY5NjAwNDgwMzYwMzgxMDE5MDYxMDFmNDkxOTA2MTI3YjA1NjViNjEwYTY1NTY1YjAwNWIzNDgwMTU2MTAyMDc1NzYwMDA4MGZkNWI1MDYxMDIyMjYwMDQ4MDM2MDM4MTAxOTA2MTAyMWQ5MTkwNjEyMjQzNTY1YjYxMGI4NTU2NWI2MDQwNTE2MTAyMmY5MTkwNjEyMmM2NTY1YjYwNDA1MTgwOTEwMzkwZjM1YjYxMDI1MjYwMDQ4MDM2MDM4MTAxOTA2MTAyNGQ5MTkwNjEyOGE0NTY1YjYxMGNhMzU2NWIwMDViMzQ4MDE1NjEwMjYwNTc2MDAwODBmZDViNTA2MTAyN2I2MDA0ODAzNjAzODEwMTkwNjEwMjc2OTE5MDYxMjI0MzU2NWI2MTBlODA1NjViNjA0MDUxNjEwMjg4OTE5MDYxMjJjNjU2NWI2MDQwNTE4MDkxMDM5MGYzNWI2MTAyYWI2MDA0ODAzNjAzODEwMTkwNjEwMmE2OTE5MDYxMmEwZjU2NWI2MTBmOWM1NjViMDA1YjYxMDJjNzYwMDQ4MDM2MDM4MTAxOTA2MTAyYzI5MTkwNjEyYWMyNTY1YjYxMTMzNDU2NWIwMDViNjEwMmUzNjAwNDgwMzYwMzgxMDE5MDYxMDJkZTkxOTA2MTJiMDI1NjViNjExNTg3NTY1YjAwNWI2MDAwODA2MDAwNjEwMTY3NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY2MzliMjNkM2Q5NjBlMDFiODg4ODg4ODg2MDQwNTE2MDI0MDE2MTAzMjI5NDkzOTI5MTkwNjEyYmJmNTY1YjYwNDA1MTYwMjA4MTgzMDMwMzgxNTI5MDYwNDA1MjkwN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE5MTY2MDIwODIwMTgwNTE3YmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmODM4MTgzMTYxNzgzNTI1MDUwNTA1MDYwNDA1MTYxMDM4YzkxOTA2MTJjNDA1NjViNjAwMDYwNDA1MTgwODMwMzgxODU1YWY0OTE1MDUwM2Q4MDYwMDA4MTE0NjEwM2M3NTc2MDQwNTE5MTUwNjAxZjE5NjAzZjNkMDExNjgyMDE2MDQwNTIzZDgyNTIzZDYwMDA2MDIwODQwMTNlNjEwM2NjNTY1YjYwNjA5MTUwNWI1MDkxNTA5MTUwODE2MTAzZGQ1NzYwMTU2MTAzZjI1NjViODA4MDYwMjAwMTkwNTE4MTAxOTA2MTAzZjE5MTkwNjEyYzkwNTY1YjViNjAwMzBiOTI1MDUwNTA5NDkzNTA1MDUwNTA1NjViNjAwMDgwNjAwMDYxMDE2NzczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2NjMxNWRhY2JlYTYwZTAxYjg4ODg4ODg4NjA0MDUxNjAyNDAxNjEwNDNlOTQ5MzkyOTE5MDYxMmJiZjU2NWI2MDQwNTE2MDIwODE4MzAzMDM4MTUyOTA2MDQwNTI5MDdiZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxOTE2NjAyMDgyMDE4MDUxN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjgzODE4MzE2MTc4MzUyNTA1MDUwNTA2MDQwNTE2MTA0YTg5MTkwNjEyYzQwNTY1YjYwMDA2MDQwNTE4MDgzMDM4MTYwMDA4NjVhZjE5MTUwNTAzZDgwNjAwMDgxMTQ2MTA0ZTU1NzYwNDA1MTkxNTA2MDFmMTk2MDNmM2QwMTE2ODIwMTYwNDA1MjNkODI1MjNkNjAwMDYwMjA4NDAxM2U2MTA0ZWE1NjViNjA2MDkxNTA1YjUwOTE1MDkxNTA4MTYxMDRmYjU3NjAxNTYxMDUxMDU2NWI4MDgwNjAyMDAxOTA1MTgxMDE5MDYxMDUwZjkxOTA2MTJjOTA1NjViNWI2MDAzMGI5MjUwNTA1MDk0OTM1MDUwNTA1MDU2NWI2MDAwNjAwNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMDUzYzU3NjEwNTNiNjEyMmZjNTY1YjViNjA0MDUxOTA4MDgyNTI4MDYwMjAwMjYwMjAwMTgyMDE2MDQwNTI4MDE1NjEwNTc1NTc4MTYwMjAwMTViNjEwNTYyNjEyMDcyNTY1YjgxNTI2MDIwMDE5MDYwMDE5MDAzOTA4MTYxMDU1YTU3OTA1MDViNTA5MDUwNjEwNTg3NjAwMDYwMDE2MDAyODk2MTE2Yzg1NjViODE2MDAwODE1MTgxMTA2MTA1OWI1NzYxMDU5YTYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTA1YjQ2MDAyNjAwMzgwODg2MTE2Yzg1NjViODE2MDAxODE1MTgxMTA2MTA1Yzg1NzYxMDVjNzYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTA1ZTA2MDA0NjAwMTg2NjExNzAxNTY1YjgxNjAwMjgxNTE4MTEwNjEwNWY0NTc2MTA1ZjM2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwNjEwNjBjNjAwNjYwMDE4NjYxMTcwMTU2NWI4MTYwMDM4MTUxODExMDYxMDYyMDU3NjEwNjFmNjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMDYzODYwMDU2MDA0ODY2MTE3MDE1NjViODE2MDA0ODE1MTgxMTA2MTA2NGM1NzYxMDY0YjYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTA2NWY2MTIwOTI1NjViNjAwMDgxNjAwMDAxOTA2MDA3MGI5MDgxNjAwNzBiODE1MjUwNTA4MzgxNjAyMDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwODI4MTYwNDAwMTkwNjAwNzBiOTA4MTYwMDcwYjgxNTI1MDUwNjEwNmM0NjEyMGNmNTY1Yjg4ODE2MDQwMDE5MDczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2OTA4MTczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2ODE1MjUwNTA4MjgxNjBlMDAxODE5MDUyNTA4MTgxNjEwMTAwMDE4MTkwNTI1MDYwMDA2MTA3MWI4YjgzNjExNzM4NTY1YjkwNTA2MDE2NjAwMzBiODExNDYxMDc2MzU3NjA0MDUxN2YwOGMzNzlhMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwODE1MjYwMDQwMTYxMDc1YTkwNjEyZDQ5NTY1YjYwNDA1MTgwOTEwMzkwZmQ1YjUwNTA1MDUwNTA1MDUwNTA1MDUwNTA1NjViNjEwNzc4NjEyMTNlNTY1YjYwMDA4MDYxMDc4NTg1ODU2MTE4NTA1NjViOTE1MDYwMDcwYjkxNTA2MDE2NjAwMzBiODIxNDYxMDc5YzU3NjAwMDgwZmQ1YjgwOTI1MDUwNTA5MjkxNTA1MDU2NWI2MDAwODA2MTAxNjc3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjYzNjE4ZGM2NWU2MGUwMWI4NTg1NjA0MDUxNjAyNDAxNjEwN2RlOTI5MTkwNjEyZGIzNTY1YjYwNDA1MTYwMjA4MTgzMDMwMzgxNTI5MDYwNDA1MjkwN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE5MTY2MDIwODIwMTgwNTE3YmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmODM4MTgzMTYxNzgzNTI1MDUwNTA1MDYwNDA1MTYxMDg0ODkxOTA2MTJjNDA1NjViNjAwMDYwNDA1MTgwODMwMzgxNjAwMDg2NWFmMTkxNTA1MDNkODA2MDAwODExNDYxMDg4NTU3NjA0MDUxOTE1MDYwMWYxOTYwM2YzZDAxMTY4MjAxNjA0MDUyM2Q4MjUyM2Q2MDAwNjAyMDg0MDEzZTYxMDg4YTU2NWI2MDYwOTE1MDViNTA5MTUwOTE1MDdmNGFmNDc4MGUwNmZlOGNiOWRmNjRiMDc5NGZhNmYwMTM5OWFmOTc5MTc1YmI5ODhlMzVlMGU1N2U1OTQ1NjdiYzgyODI2MDQwNTE2MTA4YzA5MjkxOTA2MTJkZjI1NjViNjA0MDUxODA5MTAzOTBhMTUwNTA1MDUwNTY1YjYwMDA2MDA1NjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEwOGViNTc2MTA4ZWE2MTIyZmM1NjViNWI2MDQwNTE5MDgwODI1MjgwNjAyMDAyNjAyMDAxODIwMTYwNDA1MjgwMTU2MTA5MjQ1NzgxNjAyMDAxNWI2MTA5MTE2MTIwNzI1NjViODE1MjYwMjAwMTkwNjAwMTkwMDM5MDgxNjEwOTA5NTc5MDUwNWI1MDkwNTA2MTA5MzY2MDAwNjAwMTYwMDI4NzYxMTZjODU2NWI4MTYwMDA4MTUxODExMDYxMDk0YTU3NjEwOTQ5NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMDk2MzYwMDI2MDAzODA4NjYxMTZjODU2NWI4MTYwMDE4MTUxODExMDYxMDk3NzU3NjEwOTc2NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMDk4ZjYwMDQ2MDAxODQ2MTE3MDE1NjViODE2MDAyODE1MTgxMTA2MTA5YTM=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwykbUYvqcf+uhTDVxdfvzePR9SDjXItKndn6aOjS9wWpsOe+ke0YhvwaAmIQmrqlfGgsIjOGerQYQ+4LHNyIPCgkI0OCerQYQnwgSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIA"},{"b64Body":"Cg8KCQjQ4J6tBhClCBICGAISAhgDGPfR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxj6ByKAIDU3NjEwOWEyNjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMDliYjYwMDY2MDAxODQ2MTE3MDE1NjViODE2MDAzODE1MTgxMTA2MTA5Y2Y1NzYxMDljZTYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTA5ZTc2MDA1NjAwNDg0NjExNzAxNTY1YjgxNjAwNDgxNTE4MTEwNjEwOWZiNTc2MTA5ZmE2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwNjAwMDYxMGExMjg2ODM2MTE5ODQ1NjViNjAwNzBiOTA1MDYwMTY2MDAzMGI4MTE0NjEwYTVkNTc2MDQwNTE3ZjA4YzM3OWEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA4MTUyNjAwNDAxNjEwYTU0OTA2MTJlNmU1NjViNjA0MDUxODA5MTAzOTBmZDViNTA1MDUwNTA1MDUwNTY1YjYwMDA2MDAxNjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEwYTgyNTc2MTBhODE2MTIyZmM1NjViNWI2MDQwNTE5MDgwODI1MjgwNjAyMDAyNjAyMDAxODIwMTYwNDA1MjgwMTU2MTBhYmI1NzgxNjAyMDAxNWI2MTBhYTg2MTIwNzI1NjViODE1MjYwMjAwMTkwNjAwMTkwMDM5MDgxNjEwYWEwNTc5MDUwNWI1MDkwNTA2MTBhYzY2MTIwNzI1NjViNjAwNDgxNjAwMDAxODE4MTUyNTA1MDYxMGFkOTYxMjEzZTU2NWIzMDgxNjAyMDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwNjAwMTgxNjAwMDAxOTAxNTE1OTA4MTE1MTU4MTUyNTA1MDgwODI2MDIwMDE4MTkwNTI1MDgxODM2MDAwODE1MTgxMTA2MTBiNDA1NzYxMGIzZjYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MDAwNjEwYjViMzA2MDAwODg4ODg4NjExYTljNTY1YjkwNTA2MDAwNjEwYjY5ODg4MzYxMTczODU2NWI5MDUwNjAxNjYwMDMwYjgxMTQ2MTBiN2I1NzYwMDA4MGZkNWI1MDUwNTA1MDUwNTA1MDUwNTY1YjYwMDA4MDYwMDA2MTAxNjc3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjYzOWIyM2QzZDk2MGUwMWI4ODg4ODg4ODYwNDA1MTYwMjQwMTYxMGJjMjk0OTM5MjkxOTA2MTJiYmY1NjViNjA0MDUxNjAyMDgxODMwMzAzODE1MjkwNjA0MDUyOTA3YmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTkxNjYwMjA4MjAxODA1MTdiZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmY4MzgxODMxNjE3ODM1MjUwNTA1MDUwNjA0MDUxNjEwYzJjOTE5MDYxMmM0MDU2NWI2MDAwNjA0MDUxODA4MzAzODE2MDAwODY1YWYxOTE1MDUwM2Q4MDYwMDA4MTE0NjEwYzY5NTc2MDQwNTE5MTUwNjAxZjE5NjAzZjNkMDExNjgyMDE2MDQwNTIzZDgyNTIzZDYwMDA2MDIwODQwMTNlNjEwYzZlNTY1YjYwNjA5MTUwNWI1MDkxNTA5MTUwODE2MTBjN2Y1NzYwMTU2MTBjOTQ1NjViODA4MDYwMjAwMTkwNTE4MTAxOTA2MTBjOTM5MTkwNjEyYzkwNTY1YjViNjAwMzBiOTI1MDUwNTA5NDkzNTA1MDUwNTA1NjViNjAwMDYwMDU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTBjYzA1NzYxMGNiZjYxMjJmYzU2NWI1YjYwNDA1MTkwODA4MjUyODA2MDIwMDI2MDIwMDE4MjAxNjA0MDUyODAxNTYxMGNmOTU3ODE2MDIwMDE1YjYxMGNlNjYxMjA3MjU2NWI4MTUyNjAyMDAxOTA2MDAxOTAwMzkwODE2MTBjZGU1NzkwNTA1YjUwOTA1MDYxMGQwYjYwMDA2MDAxNjAwMjhjNjExNmM4NTY1YjgxNjAwMDgxNTE4MTEwNjEwZDFmNTc2MTBkMWU2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwNjEwZDM4NjAwMjYwMDM4MDhiNjExNmM4NTY1YjgxNjAwMTgxNTE4MTEwNjEwZDRjNTc2MTBkNGI2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwNjEwZDY0NjAwNDYwMDE4OTYxMTcwMTU2NWI4MTYwMDI4MTUxODExMDYxMGQ3ODU3NjEwZDc3NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMGQ5MDYwMDY2MDAxODk2MTE3MDE1NjViODE2MDAzODE1MTgxMTA2MTBkYTQ1NzYxMGRhMzYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTBkYmM2MDA1NjAwNDg5NjExNzAxNTY1YjgxNjAwNDgxNTE4MTEwNjEwZGQwNTc2MTBkY2Y2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwODM2MDAyOTA4MTYxMGRlYTkxOTA2MTMwYTU1NjViNTA4MjYwMDM5MDgxNjEwZGZhOTE5MDYxMzBhNTU2NWI1MDgxNjAwNDkwODE2MTBlMGE5MTkwNjEzMGE1NTY1YjUwNjAwMDYxMGUxYjhiNjAwMDg5ODk4NjYxMWE5YzU2NWI5MDUwNjAwMDYxMGUyOThkODM2MTE3Mzg1NjViOTA1MDYwMTY2MDAzMGI4MTE0NjEwZTcxNTc2MDQwNTE3ZjA4YzM3OWEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA4MTUyNjAwNDAxNjEwZTY4OTA2MTJkNDk1NjViNjA0MDUxODA5MTAzOTBmZDViNTA1MDUwNTA1MDUwNTA1MDUwNTA1MDUwNTA1NjViNjAwMDgwNjAwMDYxMDE2NzczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2NjMxNWRhY2JlYTYwZTAxYjg4ODg4ODg4NjA0MDUxNjAyNDAxNjEwZWJkOTQ5MzkyOTE5MDYxMmJiZjU2NWI2MDQwNTE2MDIwODE4MzAzMDM4MTUyOTA2MDQwNTI5MDdiZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxOTE2NjAyMDgyMDE4MDUxN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjgzODE4MzE2MTc4MzUyNTA1MDUwNTA2MDQwNTE2MTBmMjc5MTkwNjEyYzQwNTY1YjYwMDA2MDQwNTE4MDgzMDM4MTg1NWFmNDkxNTA1MDNkODA2MDAwODExNDYxMGY2MjU3NjA0MDUxOTE1MDYwMWYxOTYwM2YzZDAxMTY4MjAxNjA0MDUyM2Q4MjUyM2Q2MDAwNjAyMDg0MDEzZTYxMGY2NzU2NWI2MDYwOTE1MDViNTA5MTUwOTE1MDgxNjEwZjc4NTc2MDE1NjEwZjhkNTY1YjgwODA2MDIwMDE5MDUxODEwMTkwNjEwZjhjOTE5MDYxMmM5MDU2NWI1YjYwMDMwYjkyNTA1MDUwOTQ5MzUwNTA1MDUwNTY1YjYwMDA2MDA1NjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEwZmI5NTc2MTBmYjg2MTIyZmM1NjViNWI2MDQwNTE5MDgwODI1MjgwNjAyMDAyNjAyMDAxODIwMTYwNDA1MjgwMTU2MTBmZjI1NzgxNjAyMDAxNWI2MTBmZGY2MTIwNzI1NjViODE1MjYwMjAwMTkwNjAwMTkwMDM5MDgxNjEwZmQ3NTc5MDUwNWI1MDkwNTA2MTEwMDQ2MDAwNjAwMTYwMDI4NzYxMTZjODU2NWI4MTYwMDA4MTUxODExMDYxMTAxODU3NjExMDE3NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMTAzMTYwMDI2MDAzODA4NjYxMTZjODU2NWI4MTYwMDE4MTUxODExMDYxMTA0NTU3NjExMDQ0NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMTA1ZDYwMDQ2MDAxODQ2MTE3MDE1NjViODE2MDAyODE1MTgxMTA2MTEwNzE1NzYxMTA3MDYxMmNiZDU2NWI1YjYwMjAwMjYwMjAwMTAxODE5MDUyNTA2MTEwODk2MDA2NjAwMTg0NjExNzAxNTY1YjgxNjAwMzgxNTE4MTEwNjExMDlkNTc2MTEwOWM2MTJjYmQ1NjViNWI2MDIwMDI2MDIwMDEwMTgxOTA1MjUwNjExMGI1NjAwNTYwMDQ4NDYxMTcwMTU2NWI4MTYwMDQ4MTUxODExMDYxMTBjOTU3NjExMGM4NjEyY2JkNTY1YjViNjAyMDAyNjAyMDAxMDE4MTkwNTI1MDYxMTBkYzYxMjBjZjU2NWI4NTgxNjA0MDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwNjAwMjgwNTQ2MTExMjE5MDYxMmVjODU2NWI4MDYwMWYwMTYwMjA4MDkxMDQwMjYwMjAwMTYwNDA1MTkwODEwMTYwNDA1MjgwOTI5MTkwODE4MTUyNjAyMDAxODI4MDU0NjExMTRkOTA2MTJlYzg1NjViODAxNTYxMTE5YTU3ODA2MDFmMTA2MTExNmY1NzYxMDEwMDgwODM1NDA0MDI4MzUyOTE2MDIwMDE5MTYxMTE5YTU2NWI4MjAxOTE5MDYwMDA1MjYwMjA2MDAwMjA5MDViODE1NDgxNTI5MDYwMDEwMTkwNjAyMDAxODA4MzExNjExMTdkNTc4MjkwMDM2MDFmMTY4MjAxOTE=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwoi577JmDjZgTKVbZ4vqRGhKgpD/2W2KLWq0MksST0aKAMNpRGoAxOBGflTDCMONmGgwIjOGerQYQ84SomwIiDwoJCNDgnq0GEKUIEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQjR4J6tBhCrCBICGAISAhgDGPbR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxj6ByKAIDViNTA1MDUwNTA1MDgxNjAwMDAxODE5MDUyNTA2MDAzODA1NDYxMTFiNDkwNjEyZWM4NTY1YjgwNjAxZjAxNjAyMDgwOTEwNDAyNjAyMDAxNjA0MDUxOTA4MTAxNjA0MDUyODA5MjkxOTA4MTgxNTI2MDIwMDE4MjgwNTQ2MTExZTA5MDYxMmVjODU2NWI4MDE1NjExMjJkNTc4MDYwMWYxMDYxMTIwMjU3NjEwMTAwODA4MzU0MDQwMjgzNTI5MTYwMjAwMTkxNjExMjJkNTY1YjgyMDE5MTkwNjAwMDUyNjAyMDYwMDAyMDkwNWI4MTU0ODE1MjkwNjAwMTAxOTA2MDIwMDE4MDgzMTE2MTEyMTA1NzgyOTAwMzYwMWYxNjgyMDE5MTViNTA1MDUwNTA1MDgxNjAyMDAxODE5MDUyNTA4MTgxNjBlMDAxODE5MDUyNTA2MDA0ODA1NDYxMTI1MDkwNjEyZWM4NTY1YjgwNjAxZjAxNjAyMDgwOTEwNDAyNjAyMDAxNjA0MDUxOTA4MTAxNjA0MDUyODA5MjkxOTA4MTgxNTI2MDIwMDE4MjgwNTQ2MTEyN2M5MDYxMmVjODU2NWI4MDE1NjExMmM5NTc4MDYwMWYxMDYxMTI5ZTU3NjEwMTAwODA4MzU0MDQwMjgzNTI5MTYwMjAwMTkxNjExMmM5NTY1YjgyMDE5MTkwNjAwMDUyNjAyMDYwMDAyMDkwNWI4MTU0ODE1MjkwNjAwMTAxOTA2MDIwMDE4MDgzMTE2MTEyYWM1NzgyOTAwMzYwMWYxNjgyMDE5MTViNTA1MDUwNTA1MDgxNjA2MDAxODE5MDUyNTA2MDAwNjExMmUyODg4MzYxMTczODU2NWI5MDUwNjAxNjYwMDMwYjgxMTQ2MTEzMmE1NzYwNDA1MTdmMDhjMzc5YTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgxNTI2MDA0MDE2MTEzMjE5MDYxMzFjMzU2NWI2MDQwNTE4MDkxMDM5MGZkNWI1MDUwNTA1MDUwNTA1MDUwNTY1YjYxMTMzYzYxMjBjZjU2NWI2MDAyODA1NDYxMTM0OTkwNjEyZWM4NTY1YjgwNjAxZjAxNjAyMDgwOTEwNDAyNjAyMDAxNjA0MDUxOTA4MTAxNjA0MDUyODA5MjkxOTA4MTgxNTI2MDIwMDE4MjgwNTQ2MTEzNzU5MDYxMmVjODU2NWI4MDE1NjExM2MyNTc4MDYwMWYxMDYxMTM5NzU3NjEwMTAwODA4MzU0MDQwMjgzNTI5MTYwMjAwMTkxNjExM2MyNTY1YjgyMDE5MTkwNjAwMDUyNjAyMDYwMDAyMDkwNWI4MTU0ODE1MjkwNjAwMTAxOTA2MDIwMDE4MDgzMTE2MTEzYTU1NzgyOTAwMzYwMWYxNjgyMDE5MTViNTA1MDUwNTA1MDgxNjAwMDAxODE5MDUyNTA2MDAzODA1NDYxMTNkYzkwNjEyZWM4NTY1YjgwNjAxZjAxNjAyMDgwOTEwNDAyNjAyMDAxNjA0MDUxOTA4MTAxNjA0MDUyODA5MjkxOTA4MTgxNTI2MDIwMDE4MjgwNTQ2MTE0MDg5MDYxMmVjODU2NWI4MDE1NjExNDU1NTc4MDYwMWYxMDYxMTQyYTU3NjEwMTAwODA4MzU0MDQwMjgzNTI5MTYwMjAwMTkxNjExNDU1NTY1YjgyMDE5MTkwNjAwMDUyNjAyMDYwMDAyMDkwNWI4MTU0ODE1MjkwNjAwMTAxOTA2MDIwMDE4MDgzMTE2MTE0Mzg1NzgyOTAwMzYwMWYxNjgyMDE5MTViNTA1MDUwNTA1MDgxNjAyMDAxODE5MDUyNTA4MTgxNjA0MDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwNjAwNDgwNTQ2MTE0YTc5MDYxMmVjODU2NWI4MDYwMWYwMTYwMjA4MDkxMDQwMjYwMjAwMTYwNDA1MTkwODEwMTYwNDA1MjgwOTI5MTkwODE4MTUyNjAyMDAxODI4MDU0NjExNGQzOTA2MTJlYzg1NjViODAxNTYxMTUyMDU3ODA2MDFmMTA2MTE0ZjU1NzYxMDEwMDgwODM1NDA0MDI4MzUyOTE2MDIwMDE5MTYxMTUyMDU2NWI4MjAxOTE5MDYwMDA1MjYwMjA2MDAwMjA5MDViODE1NDgxNTI5MDYwMDEwMTkwNjAyMDAxODA4MzExNjExNTAzNTc4MjkwMDM2MDFmMTY4MjAxOTE1YjUwNTA1MDUwNTA4MTYwNjAwMTgxOTA1MjUwNjAwMDYxMTUzOTg0ODM2MTE3Mzg1NjViOTA1MDYwMTY2MDAzMGI4MTE0NjExNTgxNTc2MDQwNTE3ZjA4YzM3OWEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA4MTUyNjAwNDAxNjExNTc4OTA2MTMyNTU1NjViNjA0MDUxODA5MTAzOTBmZDViNTA1MDUwNTA1NjViNjExNThmNjEyMGNmNTY1YjgyODE2MDAwMDE4MTkwNTI1MDgxODE2MDIwMDE4MTkwNTI1MDgzODE2MDQwMDE5MDczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2OTA4MTczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2ODE1MjUwNTA2MDA0ODA1NDYxMTVlNjkwNjEyZWM4NTY1YjgwNjAxZjAxNjAyMDgwOTEwNDAyNjAyMDAxNjA0MDUxOTA4MTAxNjA0MDUyODA5MjkxOTA4MTgxNTI2MDIwMDE4MjgwNTQ2MTE2MTI5MDYxMmVjODU2NWI4MDE1NjExNjVmNTc4MDYwMWYxMDYxMTYzNDU3NjEwMTAwODA4MzU0MDQwMjgzNTI5MTYwMjAwMTkxNjExNjVmNTY1YjgyMDE5MTkwNjAwMDUyNjAyMDYwMDAyMDkwNWI4MTU0ODE1MjkwNjAwMTAxOTA2MDIwMDE4MDgzMTE2MTE2NDI1NzgyOTAwMzYwMWYxNjgyMDE5MTViNTA1MDUwNTA1MDgxNjA2MDAxODE5MDUyNTA2MDAwNjExNjc4ODY4MzYxMTczODU2NWI5MDUwNjAxNjYwMDMwYjgxMTQ2MTE2YzA1NzYwNDA1MTdmMDhjMzc5YTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgxNTI2MDA0MDE2MTE2Yjc5MDYxMzJlNzU2NWI2MDQwNTE4MDkxMDM5MGZkNWI1MDUwNTA1MDUwNTA1NjViNjExNmQwNjEyMDcyNTY1YjYwNDA1MTgwNjA0MDAxNjA0MDUyODA2MTE2ZTU4Nzg3NjExZDE2NTY1YjgxNTI2MDIwMDE2MTE2ZjQ4NTg1NjExZDZjNTY1YjgxNTI1MDkwNTA5NDkzNTA1MDUwNTA1NjViNjExNzA5NjEyMDcyNTY1YjYwNDA1MTgwNjA0MDAxNjA0MDUyODA2MTE3MWQ4NjYxMWY0MTU2NWI4MTUyNjAyMDAxNjExNzJjODU4NTYxMWY4MjU2NWI4MTUyNTA5MDUwOTM5MjUwNTA1MDU2NWI2MDAwODA2MDAwNjEwMTY3NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY2MzdkMzA1Y2ZhNjBlMDFiODY4NjYwNDA1MTYwMjQwMTYxMTc3MTkyOTE5MDYxMzYwNzU2NWI2MDQwNTE2MDIwODE4MzAzMDM4MTUyOTA2MDQwNTI5MDdiZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxOTE2NjAyMDgyMDE4MDUxN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjgzODE4MzE2MTc4MzUyNTA1MDUwNTA2MDQwNTE2MTE3ZGI5MTkwNjEyYzQwNTY1YjYwMDA2MDQwNTE4MDgzMDM4MTYwMDA4NjVhZjE5MTUwNTAzZDgwNjAwMDgxMTQ2MTE4MTg1NzYwNDA1MTkxNTA2MDFmMTk2MDNmM2QwMTE2ODIwMTYwNDA1MjNkODI1MjNkNjAwMDYwMjA4NDAxM2U2MTE4MWQ1NjViNjA2MDkxNTA1YjUwOTE1MDkxNTA4MTYxMTgyZTU3NjAxNTYxMTg0MzU2NWI4MDgwNjAyMDAxOTA1MTgxMDE5MDYxMTg0MjkxOTA2MTJjOTA1NjViNWI2MDAzMGI5MjUwNTA1MDkyOTE1MDUwNTY1YjYwMDA2MTE4NWE2MTIxM2U1NjViNjAwMDgwNjEwMTY3NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY2MzNjNGRkMzJlNjBlMDFiODc4NzYwNDA1MTYwMjQwMTYxMTg5MTkyOTE5MDYxMzYzNzU2NWI2MDQwNTE2MDIwODE4MzAzMDM4MTUyOTA2MDQwNTI5MDdiZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxOTE2NjAyMDgyMDE4MDUxN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjgzODE4MzE2MTc4MzUyNTA1MDUwNTA2MDQwNTE2MTE4ZmI5MTkwNjEyYzQwNTY1YjYwMDA2MDQwNTE4MDgzMDM4MTYwMDA4NjVhZjE5MTUwNTAzZDgwNjAwMDgxMTQ2MTE5Mzg1NzYwNDA1MTkxNTA2MDFmMTk2MDNmM2QwMTE2ODIwMTYwNDA1MjNkODI1MjNkNjAwMDYwMjA4NDAxM2U2MTE5M2Q1NjViNjA2MDkxNTA1YjUwOTE1MDkxNTA2MTE5NGE2MTIxM2U1NjViODI2MTE5NTc1NzYwMTU4MTYxMTk2YzU2NWI4MTgwNjAyMDAxOTA1MTgxMDE5MDYxMTk2YjkxOTA2MTM3ZGY1NjViNWI4MTYwMDMwYjkxNTA4MDk1NTA4MTk2NTA1MDUwNTA1MDUwOTI1MDkyOTA1MDU2NWI2MDAwODA2MDAwNjEwMTY3NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmY=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIw9IuH+xyJpUyK4SFt8k7p1Js/8hK+Tolz/vY491YzhupmtNwpf68M3fHjuqny3XCDGgsIjeGerQYQy+r2YyIPCgkI0eCerQYQqwgSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIA"},{"b64Body":"Cg8KCQjR4J6tBhCxCBICGAISAhgDGPbR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxj6ByKAIGZmZmZmZmZmZmZmZmZmZmYxNjYzNmZjM2NiYWY2MGUwMWI4Njg2NjA0MDUxNjAyNDAxNjExOWJkOTI5MTkwNjEzOGMxNTY1YjYwNDA1MTYwMjA4MTgzMDMwMzgxNTI5MDYwNDA1MjkwN2JmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE5MTY2MDIwODIwMTgwNTE3YmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmODM4MTgzMTYxNzgzNTI1MDUwNTA1MDYwNDA1MTYxMWEyNzkxOTA2MTJjNDA1NjViNjAwMDYwNDA1MTgwODMwMzgxNjAwMDg2NWFmMTkxNTA1MDNkODA2MDAwODExNDYxMWE2NDU3NjA0MDUxOTE1MDYwMWYxOTYwM2YzZDAxMTY4MjAxNjA0MDUyM2Q4MjUyM2Q2MDAwNjAyMDg0MDEzZTYxMWE2OTU2NWI2MDYwOTE1MDViNTA5MTUwOTE1MDgxNjExYTdhNTc2MDE1NjExYThmNTY1YjgwODA2MDIwMDE5MDUxODEwMTkwNjExYThlOTE5MDYxMmM5MDU2NWI1YjYwMDMwYjkyNTA1MDUwOTI5MTUwNTA1NjViNjExYWE0NjEyMGNmNTY1YjYxMWFhYzYxMjA5MjU2NWI4NTgxNjAwMDAxOTA2MDA3MGI5MDgxNjAwNzBiODE1MjUwNTA4NDgxNjAyMDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwODM4MTYwNDAwMTkwNjAwNzBiOTA4MTYwMDcwYjgxNTI1MDUwNjAwMjgwNTQ2MTFiMTU5MDYxMmVjODU2NWI4MDYwMWYwMTYwMjA4MDkxMDQwMjYwMjAwMTYwNDA1MTkwODEwMTYwNDA1MjgwOTI5MTkwODE4MTUyNjAyMDAxODI4MDU0NjExYjQxOTA2MTJlYzg1NjViODAxNTYxMWI4ZTU3ODA2MDFmMTA2MTFiNjM1NzYxMDEwMDgwODM1NDA0MDI4MzUyOTE2MDIwMDE5MTYxMWI4ZTU2NWI4MjAxOTE5MDYwMDA1MjYwMjA2MDAwMjA5MDViODE1NDgxNTI5MDYwMDEwMTkwNjAyMDAxODA4MzExNjExYjcxNTc4MjkwMDM2MDFmMTY4MjAxOTE1YjUwNTA1MDUwNTA4MjYwMDAwMTgxOTA1MjUwNjAwMzgwNTQ2MTFiYTg5MDYxMmVjODU2NWI4MDYwMWYwMTYwMjA4MDkxMDQwMjYwMjAwMTYwNDA1MTkwODEwMTYwNDA1MjgwOTI5MTkwODE4MTUyNjAyMDAxODI4MDU0NjExYmQ0OTA2MTJlYzg1NjViODAxNTYxMWMyMTU3ODA2MDFmMTA2MTFiZjY1NzYxMDEwMDgwODM1NDA0MDI4MzUyOTE2MDIwMDE5MTYxMWMyMTU2NWI4MjAxOTE5MDYwMDA1MjYwMjA2MDAwMjA5MDViODE1NDgxNTI5MDYwMDEwMTkwNjAyMDAxODA4MzExNjExYzA0NTc4MjkwMDM2MDFmMTY4MjAxOTE1YjUwNTA1MDUwNTA4MjYwMjAwMTgxOTA1MjUwODY4MjYwNDAwMTkwNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY5MDgxNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTUyNTA1MDgyODI2MGUwMDE4MTkwNTI1MDgwODI2MTAxMDAwMTgxOTA1MjUwNjAwNDgwNTQ2MTFjODY5MDYxMmVjODU2NWI4MDYwMWYwMTYwMjA4MDkxMDQwMjYwMjAwMTYwNDA1MTkwODEwMTYwNDA1MjgwOTI5MTkwODE4MTUyNjAyMDAxODI4MDU0NjExY2IyOTA2MTJlYzg1NjViODAxNTYxMWNmZjU3ODA2MDFmMTA2MTFjZDQ1NzYxMDEwMDgwODM1NDA0MDI4MzUyOTE2MDIwMDE5MTYxMWNmZjU2NWI4MjAxOTE5MDYwMDA1MjYwMjA2MDAwMjA5MDViODE1NDgxNTI5MDYwMDEwMTkwNjAyMDAxODA4MzExNjExY2UyNTc4MjkwMDM2MDFmMTY4MjAxOTE1YjUwNTA1MDUwNTA4MjYwNjAwMTgxOTA1MjUwNTA5NTk0NTA1MDUwNTA1MDU2NWI2MDAwNjExZDNkODM2MDA2ODExMTE1NjExZDJlNTc2MTFkMmQ2MTM4ZjE1NjViNWI4MjYxMjA1ZTkwOTE5MDYzZmZmZmZmZmYxNjU2NWI5MDUwNjExZDY0ODI2MDA2ODExMTE1NjExZDU1NTc2MTFkNTQ2MTM4ZjE1NjViNWI4MjYxMjA1ZTkwOTE5MDYzZmZmZmZmZmYxNjU2NWI5MDUwOTI5MTUwNTA1NjViNjExZDc0NjEyMTNlNTY1YjYwMDA2MDA0ODExMTE1NjExZDg4NTc2MTFkODc2MTM4ZjE1NjViNWI4MzYwMDQ4MTExMTU2MTFkOWI1NzYxMWQ5YTYxMzhmMTU2NWI1YjAzNjExZGI2NTc2MDAxODE2MDAwMDE5MDE1MTU5MDgxMTUxNTgxNTI1MDUwNjExZjNiNTY1YjYwMDE2MDA0ODExMTE1NjExZGNhNTc2MTFkYzk2MTM4ZjE1NjViNWI4MzYwMDQ4MTExMTU2MTFkZGQ1NzYxMWRkYzYxMzhmMTU2NWI1YjAzNjExZTNmNTc2MDAwODA1NDkwNjEwMTAwMGE5MDA0NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTYwMjAwMTkwNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY5MDgxNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTUyNTA1MDYxMWYzYTU2NWI2MDAyNjAwNDgxMTExNTYxMWU1MzU3NjExZTUyNjEzOGYxNTY1YjViODM2MDA0ODExMTE1NjExZTY2NTc2MTFlNjU2MTM4ZjE1NjViNWIwMzYxMWU3OTU3ODE4MTYwNDAwMTgxOTA1MjUwNjExZjM5NTY1YjYwMDM2MDA0ODExMTE1NjExZThkNTc2MTFlOGM2MTM4ZjE1NjViNWI4MzYwMDQ4MTExMTU2MTFlYTA1NzYxMWU5ZjYxMzhmMTU2NWI1YjAzNjExZWIzNTc4MTgxNjA2MDAxODE5MDUyNTA2MTFmMzg1NjViNjAwNDgwODExMTE1NjExZWM2NTc2MTFlYzU2MTM4ZjE1NjViNWI4MzYwMDQ4MTExMTU2MTFlZDk1NzYxMWVkODYxMzhmMTU2NWI1YjAzNjExZjM3NTc2MDAwODA1NDkwNjEwMTAwMGE5MDA0NzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTYwODAwMTkwNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY5MDgxNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTUyNTA1MDViNWI1YjViNWI5MjkxNTA1MDU2NWI2MDAwNjAwMTYwMDA4MzYwMDY4MTExMTU2MTFmNWE1NzYxMWY1OTYxMzhmMTU2NWI1YjYwMDY4MTExMTU2MTFmNmM1NzYxMWY2YjYxMzhmMTU2NWI1YjgxNTI2MDIwMDE5MDgxNTI2MDIwMDE2MDAwMjA1NDkwNTA5MTkwNTA1NjViNjExZjhhNjEyMTNlNTY1YjYwMDE2MDA0ODExMTE1NjExZjllNTc2MTFmOWQ2MTM4ZjE1NjViNWI4MzYwMDQ4MTExMTU2MTFmYjE1NzYxMWZiMDYxMzhmMTU2NWI1YjAzNjExZmYzNTc4MTgxNjAyMDAxOTA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjkwODE3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI1MDUwNjEyMDU4NTY1YjYwMDQ4MDgxMTExNTYxMjAwNjU3NjEyMDA1NjEzOGYxNTY1YjViODM2MDA0ODExMTE1NjEyMDE5NTc2MTIwMTg2MTM4ZjE1NjViNWIwMzYxMjA1NzU3ODE4MTYwODAwMTkwNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY5MDgxNzNmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmMTY4MTUyNTA1MDViNWI5MjkxNTA1MDU2NWI2MDAwODE2MGZmMTY2MDAxOTAxYjgzMTc5MDUwOTI5MTUwNTA1NjViNjA0MDUxODA2MDQwMDE2MDQwNTI4MDYwMDA4MTUyNjAyMDAxNjEyMDhjNjEyMTNlNTY1YjgxNTI1MDkwNTY1YjYwNDA1MTgwNjA2MDAxNjA0MDUyODA2MDAwNjAwNzBiODE1MjYwMjAwMTYwMDA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI2MDIwMDE2MDAwNjAwNzBiODE1MjUwOTA1NjViNjA0MDUxODA2MTAxMjAwMTYwNDA1MjgwNjA2MDgxNTI2MDIwMDE2MDYwODE1MjYwMjAwMTYwMDA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI2MDIwMDE2MDYwODE1MjYwMjAwMTYwMDAxNTE1ODE1MjYwMjAwMTYwMDA2MDA3MGI4MTUyNjAyMDAxNjAwMDE1MTU4MTUyNjAyMDAxNjA2MDgxNTI2MDIwMDE2MTIxMzg2MTIwOTI1NjViODE1MjUwOTA1NjViNjA0MDUxODA2MGEwMDE2MDQwNTI4MDYwMDAxNTE1ODE1MjYwMjAwMTYwMDA3M2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYxNjgxNTI2MDIwMDE2MDYwODE1MjYwMjAwMTYwNjA4MTUyNjAyMDAxNjAwMDczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjE2ODE1MjUwOTA=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIw1jpVpBGsAQGEtF7Sji+9jZp8Eep5DZHOQc7ROPJG+ay6SqV2y0NoMwItaSixcVNoGgwIjeGerQYQu+zn5wIiDwoJCNHgnq0GELEIEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQjS4J6tBhC3CBICGAISAhgDGPbR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxj6ByKAIDU2NWI2MDAwNjA0MDUxOTA1MDkwNTY1YjYwMDA4MGZkNWI2MDAwODBmZDViNjAwMDczZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZjgyMTY5MDUwOTE5MDUwNTY1YjYwMDA2MTIxZGE4MjYxMjFhZjU2NWI5MDUwOTE5MDUwNTY1YjYxMjFlYTgxNjEyMWNmNTY1YjgxMTQ2MTIxZjU1NzYwMDA4MGZkNWI1MDU2NWI2MDAwODEzNTkwNTA2MTIyMDc4MTYxMjFlMTU2NWI5MjkxNTA1MDU2NWI2MDAwODE5MDUwOTE5MDUwNTY1YjYxMjIyMDgxNjEyMjBkNTY1YjgxMTQ2MTIyMmI1NzYwMDA4MGZkNWI1MDU2NWI2MDAwODEzNTkwNTA2MTIyM2Q4MTYxMjIxNzU2NWI5MjkxNTA1MDU2NWI2MDAwODA2MDAwODA2MDgwODU4NzAzMTIxNTYxMjI1ZDU3NjEyMjVjNjEyMWE1NTY1YjViNjAwMDYxMjI2Yjg3ODI4ODAxNjEyMWY4NTY1Yjk0NTA1MDYwMjA2MTIyN2M4NzgyODgwMTYxMjFmODU2NWI5MzUwNTA2MDQwNjEyMjhkODc4Mjg4MDE2MTIxZjg1NjViOTI1MDUwNjA2MDYxMjI5ZTg3ODI4ODAxNjEyMjJlNTY1YjkxNTA1MDkyOTU5MTk0NTA5MjUwNTY1YjYwMDA4MTYwMDcwYjkwNTA5MTkwNTA1NjViNjEyMmMwODE2MTIyYWE1NjViODI1MjUwNTA1NjViNjAwMDYwMjA4MjAxOTA1MDYxMjJkYjYwMDA4MzAxODQ2MTIyYjc1NjViOTI5MTUwNTA1NjViNjAwMDgwZmQ1YjYwMDA4MGZkNWI2MDAwNjAxZjE5NjAxZjgzMDExNjkwNTA5MTkwNTA1NjViN2Y0ZTQ4N2I3MTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDUyNjA0MTYwMDQ1MjYwMjQ2MDAwZmQ1YjYxMjMzNDgyNjEyMmViNTY1YjgxMDE4MTgxMTA2N2ZmZmZmZmZmZmZmZmZmZmY4MjExMTcxNTYxMjM1MzU3NjEyMzUyNjEyMmZjNTY1YjViODA2MDQwNTI1MDUwNTA1NjViNjAwMDYxMjM2NjYxMjE5YjU2NWI5MDUwNjEyMzcyODI4MjYxMjMyYjU2NWI5MTkwNTA1NjViNjAwMDY3ZmZmZmZmZmZmZmZmZmZmZjgyMTExNTYxMjM5MjU3NjEyMzkxNjEyMmZjNTY1YjViNjEyMzliODI2MTIyZWI1NjViOTA1MDYwMjA4MTAxOTA1MDkxOTA1MDU2NWI4MjgxODMzNzYwMDA4MzgzMDE1MjUwNTA1MDU2NWI2MDAwNjEyM2NhNjEyM2M1ODQ2MTIzNzc1NjViNjEyMzVjNTY1YjkwNTA4MjgxNTI2MDIwODEwMTg0ODQ4NDAxMTExNTYxMjNlNjU3NjEyM2U1NjEyMmU2NTY1YjViNjEyM2YxODQ4Mjg1NjEyM2E4NTY1YjUwOTM5MjUwNTA1MDU2NWI2MDAwODI2MDFmODMwMTEyNjEyNDBlNTc2MTI0MGQ2MTIyZTE1NjViNWI4MTM1NjEyNDFlODQ4MjYwMjA4NjAxNjEyM2I3NTY1YjkxNTA1MDkyOTE1MDUwNTY1YjYxMjQzMDgxNjEyMmFhNTY1YjgxMTQ2MTI0M2I1NzYwMDA4MGZkNWI1MDU2NWI2MDAwODEzNTkwNTA2MTI0NGQ4MTYxMjQyNzU2NWI5MjkxNTA1MDU2NWI2MDAwODA2MDAwODA2MDAwODA2MDAwNjBlMDg4OGEwMzEyMTU2MTI0NzI1NzYxMjQ3MTYxMjFhNTU2NWI1YjYwMDA2MTI0ODA4YTgyOGIwMTYxMjFmODU2NWI5NzUwNTA2MDIwNjEyNDkxOGE4MjhiMDE2MTIxZjg1NjViOTY1MDUwNjA0MDg4MDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjRiMjU3NjEyNGIxNjEyMWFhNTY1YjViNjEyNGJlOGE4MjhiMDE2MTIzZjk1NjViOTU1MDUwNjA2MDg4MDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjRkZjU3NjEyNGRlNjEyMWFhNTY1YjViNjEyNGViOGE4MjhiMDE2MTIzZjk1NjViOTQ1MDUwNjA4MDYxMjRmYzhhODI4YjAxNjEyMWY4NTY1YjkzNTA1MDYwYTA2MTI1MGQ4YTgyOGIwMTYxMjFmODU2NWI5MjUwNTA2MGMwNjEyNTFlOGE4MjhiMDE2MTI0M2U1NjViOTE1MDUwOTI5NTk4OTE5NDk3NTA5Mjk1NTA1NjViNjAwMDgwNjA0MDgzODUwMzEyMTU2MTI1NDQ1NzYxMjU0MzYxMjFhNTU2NWI1YjYwMDA2MTI1NTI4NTgyODYwMTYxMjFmODU2NWI5MjUwNTA2MDIwNjEyNTYzODU4Mjg2MDE2MTIyMmU1NjViOTE1MDUwOTI1MDkyOTA1MDU2NWI2MDAwODExNTE1OTA1MDkxOTA1MDU2NWI2MTI1ODI4MTYxMjU2ZDU2NWI4MjUyNTA1MDU2NWI2MTI1OTE4MTYxMjFjZjU2NWI4MjUyNTA1MDU2NWI2MDAwODE1MTkwNTA5MTkwNTA1NjViNjAwMDgyODI1MjYwMjA4MjAxOTA1MDkyOTE1MDUwNTY1YjYwMDA1YjgzODExMDE1NjEyNWQxNTc4MDgyMDE1MTgxODQwMTUyNjAyMDgxMDE5MDUwNjEyNWI2NTY1YjYwMDA4NDg0MDE1MjUwNTA1MDUwNTY1YjYwMDA2MTI1ZTg4MjYxMjU5NzU2NWI2MTI1ZjI4MTg1NjEyNWEyNTY1YjkzNTA2MTI2MDI4MTg1NjAyMDg2MDE2MTI1YjM1NjViNjEyNjBiODE2MTIyZWI1NjViODQwMTkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MGEwODMwMTYwMDA4MzAxNTE2MTI2MmU2MDAwODYwMTgyNjEyNTc5NTY1YjUwNjAyMDgzMDE1MTYxMjY0MTYwMjA4NjAxODI2MTI1ODg1NjViNTA2MDQwODMwMTUxODQ4MjAzNjA0MDg2MDE1MjYxMjY1OTgyODI2MTI1ZGQ1NjViOTE1MDUwNjA2MDgzMDE1MTg0ODIwMzYwNjA4NjAxNTI2MTI2NzM4MjgyNjEyNWRkNTY1YjkxNTA1MDYwODA4MzAxNTE2MTI2ODg2MDgwODYwMTgyNjEyNTg4NTY1YjUwODA5MTUwNTA5MjkxNTA1MDU2NWI2MDAwNjAyMDgyMDE5MDUwODE4MTAzNjAwMDgzMDE1MjYxMjZhZDgxODQ2MTI2MTY1NjViOTA1MDkyOTE1MDUwNTY1YjYwMDA4MDYwNDA4Mzg1MDMxMjE1NjEyNmNjNTc2MTI2Y2I2MTIxYTU1NjViNWI2MDAwNjEyNmRhODU4Mjg2MDE2MTIxZjg1NjViOTI1MDUwNjAyMDgzMDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjZmYjU3NjEyNmZhNjEyMWFhNTY1YjViNjEyNzA3ODU4Mjg2MDE2MTIzZjk1NjViOTE1MDUwOTI1MDkyOTA1MDU2NWI2MDAwODA2MDAwODA2MDgwODU4NzAzMTIxNTYxMjcyYjU3NjEyNzJhNjEyMWE1NTY1YjViNjAwMDYxMjczOTg3ODI4ODAxNjEyMWY4NTY1Yjk0NTA1MDYwMjA4NTAxMzU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTI3NWE1NzYxMjc1OTYxMjFhYTU2NWI1YjYxMjc2Njg3ODI4ODAxNjEyM2Y5NTY1YjkzNTA1MDYwNDA4NTAxMzU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTI3ODc1NzYxMjc4NjYxMjFhYTU2NWI1YjYxMjc5Mzg3ODI4ODAxNjEyM2Y5NTY1YjkyNTA1MDYwNjA2MTI3YTQ4NzgyODgwMTYxMjFmODU2NWI5MTUwNTA5Mjk1OTE5NDUwOTI1MDU2NWI2MDAwODA2MDAwNjA2MDg0ODYwMzEyMTU2MTI3Yzk1NzYxMjdjODYxMjFhNTU2NWI1YjYwMDA2MTI3ZDc4NjgyODcwMTYxMjFmODU2NWI5MzUwNTA2MDIwNjEyN2U4ODY4Mjg3MDE2MTIxZjg1NjViOTI1MDUwNjA0MDYxMjdmOTg2ODI4NzAxNjEyNDNlNTY1YjkxNTA1MDkyNTA5MjUwOTI1NjViNjAwMDY3ZmZmZmZmZmZmZmZmZmZmZjgyMTExNTYxMjgxZTU3NjEyODFkNjEyMmZjNTY1YjViNjEyODI3ODI2MTIyZWI1NjViOTA1MDYwMjA4MTAxOTA1MDkxOTA1MDU2NWI2MDAwNjEyODQ3NjEyODQyODQ2MTI4MDM1NjViNjEyMzVjNTY1YjkwNTA4MjgxNTI2MDIwODEwMTg0ODQ4NDAxMTExNTYxMjg2MzU3NjEyODYyNjEyMmU2NTY1YjViNjEyODZlODQ4Mjg1NjEyM2E4NTY1YjUwOTM5MjUwNTA1MDU2NWI2MDAwODI2MDFmODMwMTEyNjEyODhiNTc2MTI4OGE2MTIyZTE1NjViNWI4MTM1NjEyODliODQ4MjYwMjA4NjAxNjEyODM0NTY1YjkxNTA1MDkyOTE1MDUwNTY1YjYwMDA4MDYwMDA4MDYwMDA4MDYwMDA4MDYwMDA4MDYxMDE0MDhiOGQwMzEyMTU2MTI4Yzg1NzYxMjhjNzYxMjFhNTU2NWI1YjYwMDA2MTI4ZDY4ZDgyOGUwMTYxMjFmODU2NWI5YTUwNTA2MDIwNjEyOGU3OGQ4MjhlMDE2MTIxZjg1NjViOTk1MDUwNjA0MDhiMDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjkwODU3NjEyOTA3NjEyMWFhNTY1YjViNjEyOTE0OGQ4MjhlMDE2MTIzZjk1NjViOTg1MDUwNjA2MDhiMDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjkzNTU3NjEyOTM0NjEyMWFhNTY1YjViNjEyOTQxOGQ4MjhlMDE2MTIzZjk1NjViOTc1MDUwNjA4MDYxMjk1MjhkODI4ZTAxNjEyMWY4NTY1Yjk2NTA1MDYwYTA2MTI5NjM4ZDgyOGUwMTYxMjFmODU2NWI5NTUwNTA2MGMwNjEyOTc0OGQ4MjhlMDE2MTI0M2U1NjViOTQ1MDUwNjBlMDhiMDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjk5NTU3NjEyOTk0NjEyMWFhNTY1YjViNjEyOWExOGQ=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIw289wGGSbcef5aEbvkEzMThuecMLjoWnF74UnUMUnDEBdWs7PZ/H1XQ4DMieWaFhYGgwIjuGerQYQ6/GLswEiDwoJCNLgnq0GELcIEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQjS4J6tBhC9CBICGAISAhgDGPbR6j4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBiCASAxj6ByKAIDgyOGUwMTYxMjg3NjU2NWI5MzUwNTA2MTAxMDA4YjAxMzU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTI5YzM1NzYxMjljMjYxMjFhYTU2NWI1YjYxMjljZjhkODI4ZTAxNjEyODc2NTY1YjkyNTA1MDYxMDEyMDhiMDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMjlmMTU3NjEyOWYwNjEyMWFhNTY1YjViNjEyOWZkOGQ4MjhlMDE2MTI4NzY1NjViOTE1MDUwOTI5NTk4OWI5MTk0OTc5YTUwOTI5NTk4NTA1NjViNjAwMDgwNjAwMDgwNjAwMDYwYTA4Njg4MDMxMjE1NjEyYTJiNTc2MTJhMmE2MTIxYTU1NjViNWI2MDAwNjEyYTM5ODg4Mjg5MDE2MTIxZjg1NjViOTU1MDUwNjAyMDYxMmE0YTg4ODI4OTAxNjEyMWY4NTY1Yjk0NTA1MDYwNDA4NjAxMzU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTJhNmI1NzYxMmE2YTYxMjFhYTU2NWI1YjYxMmE3Nzg4ODI4OTAxNjEyM2Y5NTY1YjkzNTA1MDYwNjA4NjAxMzU2N2ZmZmZmZmZmZmZmZmZmZmY4MTExMTU2MTJhOTg1NzYxMmE5NzYxMjFhYTU2NWI1YjYxMmFhNDg4ODI4OTAxNjEyM2Y5NTY1YjkyNTA1MDYwODA2MTJhYjU4ODgyODkwMTYxMjFmODU2NWI5MTUwNTA5Mjk1NTA5Mjk1OTA5MzUwNTY1YjYwMDA4MDYwNDA4Mzg1MDMxMjE1NjEyYWQ5NTc2MTJhZDg2MTIxYTU1NjViNWI2MDAwNjEyYWU3ODU4Mjg2MDE2MTIxZjg1NjViOTI1MDUwNjAyMDYxMmFmODg1ODI4NjAxNjEyMWY4NTY1YjkxNTA1MDkyNTA5MjkwNTA1NjViNjAwMDgwNjAwMDgwNjA4MDg1ODcwMzEyMTU2MTJiMWM1NzYxMmIxYjYxMjFhNTU2NWI1YjYwMDA2MTJiMmE4NzgyODgwMTYxMjFmODU2NWI5NDUwNTA2MDIwNjEyYjNiODc4Mjg4MDE2MTIxZjg1NjViOTM1MDUwNjA0MDg1MDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMmI1YzU3NjEyYjViNjEyMWFhNTY1YjViNjEyYjY4ODc4Mjg4MDE2MTI4NzY1NjViOTI1MDUwNjA2MDg1MDEzNTY3ZmZmZmZmZmZmZmZmZmZmZjgxMTExNTYxMmI4OTU3NjEyYjg4NjEyMWFhNTY1YjViNjEyYjk1ODc4Mjg4MDE2MTI4NzY1NjViOTE1MDUwOTI5NTkxOTQ1MDkyNTA1NjViNjEyYmFhODE2MTIxY2Y1NjViODI1MjUwNTA1NjViNjEyYmI5ODE2MTIyMGQ1NjViODI1MjUwNTA1NjViNjAwMDYwODA4MjAxOTA1MDYxMmJkNDYwMDA4MzAxODc2MTJiYTE1NjViNjEyYmUxNjAyMDgzMDE4NjYxMmJhMTU2NWI2MTJiZWU2MDQwODMwMTg1NjEyYmExNTY1YjYxMmJmYjYwNjA4MzAxODQ2MTJiYjA1NjViOTU5NDUwNTA1MDUwNTA1NjViNjAwMDgxOTA1MDkyOTE1MDUwNTY1YjYwMDA2MTJjMWE4MjYxMjU5NzU2NWI2MTJjMjQ4MTg1NjEyYzA0NTY1YjkzNTA2MTJjMzQ4MTg1NjAyMDg2MDE2MTI1YjM1NjViODA4NDAxOTE1MDUwOTI5MTUwNTA1NjViNjAwMDYxMmM0YzgyODQ2MTJjMGY1NjViOTE1MDgxOTA1MDkyOTE1MDUwNTY1YjYwMDA4MTYwMDMwYjkwNTA5MTkwNTA1NjViNjEyYzZkODE2MTJjNTc1NjViODExNDYxMmM3ODU3NjAwMDgwZmQ1YjUwNTY1YjYwMDA4MTUxOTA1MDYxMmM4YTgxNjEyYzY0NTY1YjkyOTE1MDUwNTY1YjYwMDA2MDIwODI4NDAzMTIxNTYxMmNhNjU3NjEyY2E1NjEyMWE1NTY1YjViNjAwMDYxMmNiNDg0ODI4NTAxNjEyYzdiNTY1YjkxNTA1MDkyOTE1MDUwNTY1YjdmNGU0ODdiNzEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDYwMDA1MjYwMzI2MDA0NTI2MDI0NjAwMGZkNWI2MDAwODI4MjUyNjAyMDgyMDE5MDUwOTI5MTUwNTA1NjViN2Y1NTcwNjQ2MTc0NjUyMDZmNjYyMDc0NmY2YjY1NmU0OTZlNjY2ZjIwNjY2MTY5NmM2NTY0MjEwMDAwMDAwMDAwNjAwMDgyMDE1MjUwNTY1YjYwMDA2MTJkMzM2MDFiODM2MTJjZWM1NjViOTE1MDYxMmQzZTgyNjEyY2ZkNTY1YjYwMjA4MjAxOTA1MDkxOTA1MDU2NWI2MDAwNjAyMDgyMDE5MDUwODE4MTAzNjAwMDgzMDE1MjYxMmQ2MjgxNjEyZDI2NTY1YjkwNTA5MTkwNTA1NjViNjAwMDgyODI1MjYwMjA4MjAxOTA1MDkyOTE1MDUwNTY1YjYwMDA2MTJkODU4MjYxMjU5NzU2NWI2MTJkOGY4MTg1NjEyZDY5NTY1YjkzNTA2MTJkOWY4MTg1NjAyMDg2MDE2MTI1YjM1NjViNjEyZGE4ODE2MTIyZWI1NjViODQwMTkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MDQwODIwMTkwNTA2MTJkYzg2MDAwODMwMTg1NjEyYmExNTY1YjgxODEwMzYwMjA4MzAxNTI2MTJkZGE4MTg0NjEyZDdhNTY1YjkwNTA5MzkyNTA1MDUwNTY1YjYxMmRlYzgxNjEyNTZkNTY1YjgyNTI1MDUwNTY1YjYwMDA2MDQwODIwMTkwNTA2MTJlMDc2MDAwODMwMTg1NjEyZGUzNTY1YjgxODEwMzYwMjA4MzAxNTI2MTJlMTk4MTg0NjEyZDdhNTY1YjkwNTA5MzkyNTA1MDUwNTY1YjdmNTU3MDY0NjE3NDY1MjA2ZjY2MjA3NDZmNmI2NTZlMjA2YjY1Nzk3MzIwNjY2MTY5NmM2NTY0MjEwMDAwMDAwMDYwMDA4MjAxNTI1MDU2NWI2MDAwNjEyZTU4NjAxYzgzNjEyY2VjNTY1YjkxNTA2MTJlNjM4MjYxMmUyMjU2NWI2MDIwODIwMTkwNTA5MTkwNTA1NjViNjAwMDYwMjA4MjAxOTA1MDgxODEwMzYwMDA4MzAxNTI2MTJlODc4MTYxMmU0YjU2NWI5MDUwOTE5MDUwNTY1YjYwMDA4MTUxOTA1MDkxOTA1MDU2NWI3ZjRlNDg3YjcxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA2MDAwNTI2MDIyNjAwNDUyNjAyNDYwMDBmZDViNjAwMDYwMDI4MjA0OTA1MDYwMDE4MjE2ODA2MTJlZTA1NzYwN2Y4MjE2OTE1MDViNjAyMDgyMTA4MTAzNjEyZWYzNTc2MTJlZjI2MTJlOTk1NjViNWI1MDkxOTA1MDU2NWI2MDAwODE5MDUwODE2MDAwNTI2MDIwNjAwMDIwOTA1MDkxOTA1MDU2NWI2MDAwNjAyMDYwMWY4MzAxMDQ5MDUwOTE5MDUwNTY1YjYwMDA4MjgyMWI5MDUwOTI5MTUwNTA1NjViNjAwMDYwMDg4MzAyNjEyZjViN2ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmODI2MTJmMWU1NjViNjEyZjY1ODY4MzYxMmYxZTU2NWI5NTUwODAxOTg0MTY5MzUwODA4NjE2ODQxNzkyNTA1MDUwOTM5MjUwNTA1MDU2NWI2MDAwODE5MDUwOTE5MDUwNTY1YjYwMDA2MTJmYTI2MTJmOWQ2MTJmOTg4NDYxMjIwZDU2NWI2MTJmN2Q1NjViNjEyMjBkNTY1YjkwNTA5MTkwNTA1NjViNjAwMDgxOTA1MDkxOTA1MDU2NWI2MTJmYmM4MzYxMmY4NzU2NWI2MTJmZDA2MTJmYzg4MjYxMmZhOTU2NWI4NDg0NTQ2MTJmMmI1NjViODI1NTUwNTA1MDUwNTY1YjYwMDA5MDU2NWI2MTJmZTU2MTJmZDg1NjViNjEyZmYwODE4NDg0NjEyZmIzNTY1YjUwNTA1MDU2NWI1YjgxODExMDE1NjEzMDE0NTc2MTMwMDk2MDAwODI2MTJmZGQ1NjViNjAwMTgxMDE5MDUwNjEyZmY2NTY1YjUwNTA1NjViNjAxZjgyMTExNTYxMzA1OTU3NjEzMDJhODE2MTJlZjk1NjViNjEzMDMzODQ2MTJmMGU1NjViODEwMTYwMjA4NTEwMTU2MTMwNDI1NzgxOTA1MDViNjEzMDU2NjEzMDRlODU2MTJmMGU1NjViODMwMTgyNjEyZmY1NTY1YjUwNTA1YjUwNTA1MDU2NWI2MDAwODI4MjFjOTA1MDkyOTE1MDUwNTY1YjYwMDA2MTMwN2M2MDAwMTk4NDYwMDgwMjYxMzA1ZTU2NWIxOTgwODMxNjkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MTMwOTU4MzgzNjEzMDZiNTY1YjkxNTA4MjYwMDIwMjgyMTc5MDUwOTI5MTUwNTA1NjViNjEzMGFlODI2MTJlOGU1NjViNjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEzMGM3NTc2MTMwYzY2MTIyZmM1NjViNWI2MTMwZDE4MjU0NjEyZWM4NTY1YjYxMzBkYzgyODI4NTYxMzAxODU2NWI2MDAwNjAyMDkwNTA2MDFmODMxMTYwMDE4MTE0NjEzMTBmNTc2MDAwODQxNTYxMzBmZDU3ODI4NzAxNTE5MDUwNWI2MTMxMDc4NTgyNjEzMDg5NTY1Yjg2NTU1MDYxMzE2ZjU2NWI2MDFmMTk4NDE2NjEzMTFkODY2MTJlZjk1NjViNjAwMDViODI4MTEwMTU2MTMxNDU1Nzg0ODkwMTUxODI1NTYwMDE4MjAxOTE1MDYwMjA4NTAxOTQ1MDYwMjA4MTAxOTA1MDYxMzEyMDU2NWI4NjgzMTAxNTYxMzE2MjU3ODQ4OTAxNTE2MTMxNWU2MDFmODkxNjgyNjEzMDZiNTY1YjgzNTU1MDViNjAwMTYwMDI4ODAyMDE4ODU1NTA1MDUwNWI1MDUwNTA1MDUwNTA1NjViN2Y1NTcwNjQ2MTc0NjUyMDZmNjYyMDc0NmY2YjY1NmU0OTZlNjY2ZjIwNmI2NTc5NzMyMDY2NjE2OTZjNjU2NDIxNjA=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwy6QH5gCEbc7z10msAq3iGM+OPRNHAQSoWHQyoaVniDsSmBG4bqTPhyrSotfZ7eT0GgwIjuGerQYQk+/lpQMiDwoJCNLgnq0GEL0IEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQjT4J6tBhDDCBICGAISAhgDGPKjnj4iAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjoIBgB8SAxj6ByL4HjAwODIwMTUyNTA1NjViNjAwMDYxMzFhZDYwMjA4MzYxMmNlYzU2NWI5MTUwNjEzMWI4ODI2MTMxNzc1NjViNjAyMDgyMDE5MDUwOTE5MDUwNTY1YjYwMDA2MDIwODIwMTkwNTA4MTgxMDM2MDAwODMwMTUyNjEzMWRjODE2MTMxYTA1NjViOTA1MDkxOTA1MDU2NWI3ZjU1NzA2NDYxNzQ2NTIwNmY2NjIwNzQ2ZjZiNjU2ZTQ5NmU2NjZmMmU3NDcyNjU2MTczNzU3Mjc5MjA2NjYxNjk2MDAwODIwMTUyN2Y2YzY1NjQyMTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAyMDgyMDE1MjUwNTY1YjYwMDA2MTMyM2Y2MDI0ODM2MTJjZWM1NjViOTE1MDYxMzI0YTgyNjEzMWUzNTY1YjYwNDA4MjAxOTA1MDkxOTA1MDU2NWI2MDAwNjAyMDgyMDE5MDUwODE4MTAzNjAwMDgzMDE1MjYxMzI2ZTgxNjEzMjMyNTY1YjkwNTA5MTkwNTA1NjViN2Y1NTcwNjQ2MTc0NjUyMDZmNjYyMDc0NmY2YjY1NmU0OTZlNjY2ZjIwNmU2MTZkNjUyMDYxNmU2NDIwNzM3OTZkNjAwMDgyMDE1MjdmNjI2ZjZjMjA2NjYxNjk2YzY1NjQyMTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDYwMjA4MjAxNTI1MDU2NWI2MDAwNjEzMmQxNjAyYjgzNjEyY2VjNTY1YjkxNTA2MTMyZGM4MjYxMzI3NTU2NWI2MDQwODIwMTkwNTA5MTkwNTA1NjViNjAwMDYwMjA4MjAxOTA1MDgxODEwMzYwMDA4MzAxNTI2MTMzMDA4MTYxMzJjNDU2NWI5MDUwOTE5MDUwNTY1YjYwMDA4MjgyNTI2MDIwODIwMTkwNTA5MjkxNTA1MDU2NWI2MDAwNjEzMzIzODI2MTJlOGU1NjViNjEzMzJkODE4NTYxMzMwNzU2NWI5MzUwNjEzMzNkODE4NTYwMjA4NjAxNjEyNWIzNTY1YjYxMzM0NjgxNjEyMmViNTY1Yjg0MDE5MTUwNTA5MjkxNTA1MDU2NWI2MTMzNWE4MTYxMjJhYTU2NWI4MjUyNTA1MDU2NWI2MDAwODE1MTkwNTA5MTkwNTA1NjViNjAwMDgyODI1MjYwMjA4MjAxOTA1MDkyOTE1MDUwNTY1YjYwMDA4MTkwNTA2MDIwODIwMTkwNTA5MTkwNTA1NjViNjEzMzk1ODE2MTIyMGQ1NjViODI1MjUwNTA1NjViNjAwMDYwYTA4MzAxNjAwMDgzMDE1MTYxMzNiMzYwMDA4NjAxODI2MTI1Nzk1NjViNTA2MDIwODMwMTUxNjEzM2M2NjAyMDg2MDE4MjYxMjU4ODU2NWI1MDYwNDA4MzAxNTE4NDgyMDM2MDQwODYwMTUyNjEzM2RlODI4MjYxMjVkZDU2NWI5MTUwNTA2MDYwODMwMTUxODQ4MjAzNjA2MDg2MDE1MjYxMzNmODgyODI2MTI1ZGQ1NjViOTE1MDUwNjA4MDgzMDE1MTYxMzQwZDYwODA4NjAxODI2MTI1ODg1NjViNTA4MDkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MDQwODMwMTYwMDA4MzAxNTE2MTM0MzA2MDAwODYwMTgyNjEzMzhjNTY1YjUwNjAyMDgzMDE1MTg0ODIwMzYwMjA4NjAxNTI2MTM0NDg4MjgyNjEzMzliNTY1YjkxNTA1MDgwOTE1MDUwOTI5MTUwNTA1NjViNjAwMDYxMzQ2MTgzODM2MTM0MTg1NjViOTA1MDkyOTE1MDUwNTY1YjYwMDA2MDIwODIwMTkwNTA5MTkwNTA1NjViNjAwMDYxMzQ4MTgyNjEzMzYwNTY1YjYxMzQ4YjgxODU2MTMzNmI1NjViOTM1MDgzNjAyMDgyMDI4NTAxNjEzNDlkODU2MTMzN2M1NjViODA2MDAwNWI4NTgxMTAxNTYxMzRkOTU3ODQ4NDAzODk1MjgxNTE2MTM0YmE4NTgyNjEzNDU1NTY1Yjk0NTA2MTM0YzU4MzYxMzQ2OTU2NWI5MjUwNjAyMDhhMDE5OTUwNTA2MDAxODEwMTkwNTA2MTM0YTE1NjViNTA4Mjk3NTA4Nzk1NTA1MDUwNTA1MDUwOTI5MTUwNTA1NjViNjA2MDgyMDE2MDAwODIwMTUxNjEzNTAxNjAwMDg1MDE4MjYxMzM1MTU2NWI1MDYwMjA4MjAxNTE2MTM1MTQ2MDIwODUwMTgyNjEyNTg4NTY1YjUwNjA0MDgyMDE1MTYxMzUyNzYwNDA4NTAxODI2MTMzNTE1NjViNTA1MDUwNTA1NjViNjAwMDYxMDE2MDgzMDE2MDAwODMwMTUxODQ4MjAzNjAwMDg2MDE1MjYxMzU0YjgyODI2MTMzMTg1NjViOTE1MDUwNjAyMDgzMDE1MTg0ODIwMzYwMjA4NjAxNTI2MTM1NjU4MjgyNjEzMzE4NTY1YjkxNTA1MDYwNDA4MzAxNTE2MTM1N2E2MDQwODYwMTgyNjEyNTg4NTY1YjUwNjA2MDgzMDE1MTg0ODIwMzYwNjA4NjAxNTI2MTM1OTI4MjgyNjEzMzE4NTY1YjkxNTA1MDYwODA4MzAxNTE2MTM1YTc2MDgwODYwMTgyNjEyNTc5NTY1YjUwNjBhMDgzMDE1MTYxMzViYTYwYTA4NjAxODI2MTMzNTE1NjViNTA2MGMwODMwMTUxNjEzNWNkNjBjMDg2MDE4MjYxMjU3OTU2NWI1MDYwZTA4MzAxNTE4NDgyMDM2MGUwODYwMTUyNjEzNWU1ODI4MjYxMzQ3NjU2NWI5MTUwNTA2MTAxMDA4MzAxNTE2MTM1ZmM2MTAxMDA4NjAxODI2MTM0ZWI1NjViNTA4MDkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MDQwODIwMTkwNTA2MTM2MWM2MDAwODMwMTg1NjEyYmExNTY1YjgxODEwMzYwMjA4MzAxNTI2MTM2MmU4MTg0NjEzNTJkNTY1YjkwNTA5MzkyNTA1MDUwNTY1YjYwMDA2MDQwODIwMTkwNTA2MTM2NGM2MDAwODMwMTg1NjEyYmExNTY1YjYxMzY1OTYwMjA4MzAxODQ2MTJiYjA1NjViOTM5MjUwNTA1MDU2NWI2MDAwODBmZDViNjAwMDgwZmQ1YjYxMzY3MzgxNjEyNTZkNTY1YjgxMTQ2MTM2N2U1NzYwMDA4MGZkNWI1MDU2NWI2MDAwODE1MTkwNTA2MTM2OTA4MTYxMzY2YTU2NWI5MjkxNTA1MDU2NWI2MDAwODE1MTkwNTA2MTM2YTU4MTYxMjFlMTU2NWI5MjkxNTA1MDU2NWI2MDAwNjEzNmJlNjEzNmI5ODQ2MTIzNzc1NjViNjEyMzVjNTY1YjkwNTA4MjgxNTI2MDIwODEwMTg0ODQ4NDAxMTExNTYxMzZkYTU3NjEzNmQ5NjEyMmU2NTY1YjViNjEzNmU1ODQ4Mjg1NjEyNWIzNTY1YjUwOTM5MjUwNTA1MDU2NWI2MDAwODI2MDFmODMwMTEyNjEzNzAyNTc2MTM3MDE2MTIyZTE1NjViNWI4MTUxNjEzNzEyODQ4MjYwMjA4NjAxNjEzNmFiNTY1YjkxNTA1MDkyOTE1MDUwNTY1YjYwMDA2MGEwODI4NDAzMTIxNTYxMzczMTU3NjEzNzMwNjEzNjYwNTY1YjViNjEzNzNiNjBhMDYxMjM1YzU2NWI5MDUwNjAwMDYxMzc0Yjg0ODI4NTAxNjEzNjgxNTY1YjYwMDA4MzAxNTI1MDYwMjA2MTM3NWY4NDgyODUwMTYxMzY5NjU2NWI2MDIwODMwMTUyNTA2MDQwODIwMTUxNjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEzNzgzNTc2MTM3ODI2MTM2NjU1NjViNWI2MTM3OGY4NDgyODUwMTYxMzZlZDU2NWI2MDQwODMwMTUyNTA2MDYwODIwMTUxNjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEzN2IzNTc2MTM3YjI2MTM2NjU1NjViNWI2MTM3YmY4NDgyODUwMTYxMzZlZDU2NWI2MDYwODMwMTUyNTA2MDgwNjEzN2QzODQ4Mjg1MDE2MTM2OTY1NjViNjA4MDgzMDE1MjUwOTI5MTUwNTA1NjViNjAwMDgwNjA0MDgzODUwMzEyMTU2MTM3ZjY1NzYxMzdmNTYxMjFhNTU2NWI1YjYwMDA2MTM4MDQ4NTgyODYwMTYxMmM3YjU2NWI5MjUwNTA2MDIwODMwMTUxNjdmZmZmZmZmZmZmZmZmZmZmODExMTE1NjEzODI1NTc2MTM4MjQ2MTIxYWE1NjViNWI2MTM4MzE4NTgyODYwMTYxMzcxYjU2NWI5MTUwNTA5MjUwOTI5MDUwNTY1YjYwMDA4MjgyNTI2MDIwODIwMTkwNTA5MjkxNTA1MDU2NWI2MDAwNjEzODU3ODI2MTMzNjA1NjViNjEzODYxODE4NTYxMzgzYjU2NWI5MzUwODM2MDIwODIwMjg1MDE2MTM4NzM4NTYxMzM3YzU2NWI4MDYwMDA1Yjg1ODExMDE1NjEzOGFmNTc4NDg0MDM4OTUyODE1MTYxMzg5MDg1ODI2MTM0NTU1NjViOTQ1MDYxMzg5YjgzNjEzNDY5NTY1YjkyNTA2MDIwOGEwMTk5NTA1MDYwMDE4MTAxOTA1MDYxMzg3NzU2NWI1MDgyOTc1MDg3OTU1MDUwNTA1MDUwNTA5MjkxNTA1MDU2NWI2MDAwNjA0MDgyMDE5MDUwNjEzOGQ2NjAwMDgzMDE4NTYxMmJhMTU2NWI4MTgxMDM2MDIwODMwMTUyNjEzOGU4ODE4NDYxMzg0YzU2NWI5MDUwOTM5MjUwNTA1MDU2NWI3ZjRlNDg3YjcxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA2MDAwNTI2MDIxNjAwNDUyNjAyNDYwMDBmZGZlYTI2NDY5NzA2NjczNTgyMjEyMjBlZDAxNzAwYmU1OTIxNmVjMDMxNjViOGEyMWZkOTVjY2NmOTQ3NmM1NmI5M2FiNTk5Nzg3MmE4ZGZkZWNiYTY0NjQ3MzZmNmM2MzQzMDAwODEyMDAzMw==","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwIQAZfNYFpZi2CixgzFa8eD2TV+T9wLQ7Ue6NH/Lpsduz0rkaOnwcSeqgolrTzL4sGgwIj+GerQYQg4ub1AEiDwoJCNPgnq0GEMMIEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQjT4J6tBhDFCBICGAISAhgDGJb7rp0CIgIIeDIgw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5CRgoDGPoHGiISIFubmvxDH2eR+bvtkrFPRL8rNvEfUIcGEOodOl0vth4JIICS9AFCBQiAztoDUgBaAGoLY2VsbGFyIGRvb3I=","b64Record":"CiUIFiIDGPsHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjADGtTy9j1SPdUQ7QPIz4qokUCRyFXOvuQQiRNBYNa3M8UzEAF9juCs626fhi1m5YwaDAiP4Z6tBhD7hLHDAyIPCgkI0+CerQYQxQgSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjjCAmKtsQox1CgMY+wcS1nJggGBAUmAENhBhAMJXYAA1YOAcgGN9y7xyEWEAf1eAY+rG8/4RYQBZV4Bj6sbz/hRhAlRXgGPrVI7/FGECkVeAY/AHKZAUYQKtV4Bj8XMHYBRhAslXYQDCVluAY33LvHIUYQHfV4BjmyPT2RRhAftXgGPmCOGNFGECOFdhAMJWW4BjEeH8BxRhAMdXgGMV2svqFGEBBFeAYzqhLvUUYQFBV4BjQSm32xRhAV1XgGNhjcZeFGEBmleAY3j+cqEUYQHDV1tgAID9WzSAFWEA01dgAID9W1BhAO5gBIA2A4EBkGEA6ZGQYSJDVlthAuVWW2BAUWEA+5GQYSLGVltgQFGAkQOQ81s0gBVhARBXYACA/VtQYQErYASANgOBAZBhASaRkGEiQ1ZbYQQBVltgQFFhATiRkGEixlZbYEBRgJEDkPNbYQFbYASANgOBAZBhAVaRkGEkU1ZbYQUfVlsAWzSAFWEBaVdgAID9W1BhAYRgBIA2A4EBkGEBf5GQYSUtVlthB3BWW2BAUWEBkZGQYSaTVltgQFGAkQOQ81s0gBVhAaZXYACA/VtQYQHBYASANgOBAZBhAbyRkGEmtVZbYQenVlsAW2EB3WAEgDYDgQGQYQHYkZBhJxFWW2EIzlZbAFthAflgBIA2A4EBkGEB9JGQYSewVlthCmVWWwBbNIAVYQIHV2AAgP1bUGECImAEgDYDgQGQYQIdkZBhIkNWW2ELhVZbYEBRYQIvkZBhIsZWW2BAUYCRA5DzW2ECUmAEgDYDgQGQYQJNkZBhKKRWW2EMo1ZbAFs0gBVhAmBXYACA/VtQYQJ7YASANgOBAZBhAnaRkGEiQ1ZbYQ6AVltgQFFhAoiRkGEixlZbYEBRgJEDkPNbYQKrYASANgOBAZBhAqaRkGEqD1ZbYQ+cVlsAW2ECx2AEgDYDgQGQYQLCkZBhKsJWW2ETNFZbAFthAuNgBIA2A4EBkGEC3pGQYSsCVlthFYdWWwBbYACAYABhAWdz//////////////////////////8WY5sj09lg4BuIiIiIYEBRYCQBYQMilJOSkZBhK79WW2BAUWAggYMDA4FSkGBAUpB7/////////////////////////////////////xkWYCCCAYBRe/////////////////////////////////////+DgYMWF4NSUFBQUGBAUWEDjJGQYSxAVltgAGBAUYCDA4GFWvSRUFA9gGAAgRRhA8dXYEBRkVBgHxlgPz0BFoIBYEBSPYJSPWAAYCCEAT5hA8xWW2BgkVBbUJFQkVCBYQPdV2AVYQPyVluAgGAgAZBRgQGQYQPxkZBhLJBWW1tgAwuSUFBQlJNQUFBQVltgAIBgAGEBZ3P//////////////////////////xZjFdrL6mDgG4iIiIhgQFFgJAFhBD6Uk5KRkGErv1ZbYEBRYCCBgwMDgVKQYEBSkHv/////////////////////////////////////GRZgIIIBgFF7/////////////////////////////////////4OBgxYXg1JQUFBQYEBRYQSokZBhLEBWW2AAYEBRgIMDgWAAhlrxkVBQPYBgAIEUYQTlV2BAUZFQYB8ZYD89ARaCAWBAUj2CUj1gAGAghAE+YQTqVltgYJFQW1CRUJFQgWEE+1dgFWEFEFZbgIBgIAGQUYEBkGEFD5GQYSyQVltbYAMLklBQUJSTUFBQUFZbYABgBWf//////////4ERFWEFPFdhBTthIvxWW1tgQFGQgIJSgGAgAmAgAYIBYEBSgBVhBXVXgWAgAVthBWJhIHJWW4FSYCABkGABkAOQgWEFWleQUFtQkFBhBYdgAGABYAKJYRbIVluBYACBUYEQYQWbV2EFmmEsvVZbW2AgAmAgAQGBkFJQYQW0YAJgA4CIYRbIVluBYAGBUYEQYQXIV2EFx2EsvVZbW2AgAmAgAQGBkFJQYQXgYARgAYZhFwFWW4FgAoFRgRBhBfRXYQXzYSy9VltbYCACYCABAYGQUlBhBgxgBmABhmEXAVZbgWADgVGBEGEGIFdhBh9hLL1WW1tgIAJgIAEBgZBSUGEGOGAFYASGYRcBVluBYASBUYEQYQZMV2EGS2EsvVZbW2AgAmAgAQGBkFJQYQZfYSCSVltgAIFgAAGQYAcLkIFgBwuBUlBQg4FgIAGQc///////////////////////////FpCBc///////////////////////////FoFSUFCCgWBAAZBgBwuQgWAHC4FSUFBhBsRhIM9WW4iBYEABkHP//////////////////////////xaQgXP//////////////////////////xaBUlBQgoFg4AGBkFJQgYFhAQABgZBSUGAAYQcbi4NhFzhWW5BQYBZgAwuBFGEHY1dgQFF/CMN5oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBUmAEAWEHWpBhLUlWW2BAUYCRA5D9W1BQUFBQUFBQUFBQVlthB3hhIT5WW2AAgGEHhYWFYRhQVluRUGAHC5FQYBZgAwuCFGEHnFdgAID9W4CSUFBQkpFQUFZbYACAYQFnc///////////////////////////FmNhjcZeYOAbhYVgQFFgJAFhB96SkZBhLbNWW2BAUWAggYMDA4FSkGBAUpB7/////////////////////////////////////xkWYCCCAYBRe/////////////////////////////////////+DgYMWF4NSUFBQUGBAUWEISJGQYSxAVltgAGBAUYCDA4FgAIZa8ZFQUD2AYACBFGEIhVdgQFGRUGAfGWA/PQEWggFgQFI9glI9YABgIIQBPmEIilZbYGCRUFtQkVCRUH9K9HgOBv6Mud9ksHlPpvATma+XkXW7mI414OV+WUVnvIKCYEBRYQjAkpGQYS3yVltgQFGAkQOQoVBQUFBWW2AAYAVn//////////+BERVhCOtXYQjqYSL8VltbYEBRkICCUoBgIAJgIAGCAWBAUoAVYQkkV4FgIAFbYQkRYSByVluBUmAgAZBgAZADkIFhCQlXkFBbUJBQYQk2YABgAWACh2EWyFZbgWAAgVGBEGEJSldhCUlhLL1WW1tgIAJgIAEBgZBSUGEJY2ACYAOAhmEWyFZbgWABgVGBEGEJd1dhCXZhLL1WW1tgIAJgIAEBgZBSUGEJj2AEYAGEYRcBVluBYAKBUYEQYQmjV2EJomEsvVZbW2AgAmAgAQGBkFJQYQm7YAZgAYRhFwFWW4FgA4FRgRBhCc9XYQnOYSy9VltbYCACYCABAYGQUlBhCedgBWAEhGEXAVZbgWAEgVGBEGEJ+1dhCfphLL1WW1tgIAJgIAEBgZBSUGAAYQoShoNhGYRWW2AHC5BQYBZgAwuBFGEKXVdgQFF/CMN5oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBUmAEAWEKVJBhLm5WW2BAUYCRA5D9W1BQUFBQUFZbYABgAWf//////////4ERFWEKgldhCoFhIvxWW1tgQFGQgIJSgGAgAmAgAYIBYEBSgBVhCrtXgWAgAVthCqhhIHJWW4FSYCABkGABkAOQgWEKoFeQUFtQkFBhCsZhIHJWW2AEgWAAAYGBUlBQYQrZYSE+VlswgWAgAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUGABgWAAAZAVFZCBFRWBUlBQgIJgIAGBkFJQgYNgAIFRgRBhC0BXYQs/YSy9VltbYCACYCABAYGQUlBgAGELWzBgAIiIiGEanFZbkFBgAGELaYiDYRc4VluQUGAWYAMLgRRhC3tXYACA/VtQUFBQUFBQUFZbYACAYABhAWdz//////////////////////////8WY5sj09lg4BuIiIiIYEBRYCQBYQvClJOSkZBhK79WW2BAUWAggYMDA4FSkGBAUpB7/////////////////////////////////////xkWYCCCAYBRe/////////////////////////////////////+DgYMWF4NSUFBQUGBAUWEMLJGQYSxAVltgAGBAUYCDA4FgAIZa8ZFQUD2AYACBFGEMaVdgQFGRUGAfGWA/PQEWggFgQFI9glI9YABgIIQBPmEMblZbYGCRUFtQkVCRUIFhDH9XYBVhDJRWW4CAYCABkFGBAZBhDJORkGEskFZbW2ADC5JQUFCUk1BQUFBWW2AAYAVn//////////+BERVhDMBXYQy/YSL8VltbYEBRkICCUoBgIAJgIAGCAWBAUoAVYQz5V4FgIAFbYQzmYSByVluBUmAgAZBgAZADkIFhDN5XkFBbUJBQYQ0LYABgAWACjGEWyFZbgWAAgVGBEGENH1dhDR5hLL1WW1tgIAJgIAEBgZBSUGENOGACYAOAi2EWyFZbgWABgVGBEGENTFdhDUthLL1WW1tgIAJgIAEBgZBSUGENZGAEYAGJYRcBVluBYAKBUYEQYQ14V2ENd2EsvVZbW2AgAmAgAQGBkFJQYQ2QYAZgAYlhFwFWW4FgA4FRgRBhDaRXYQ2jYSy9VltbYCACYCABAYGQUlBhDbxgBWAEiWEXAVZbgWAEgVGBEGEN0FdhDc9hLL1WW1tgIAJgIAEBgZBSUINgApCBYQ3qkZBhMKVWW1CCYAOQgWEN+pGQYTClVltQgWAEkIFhDgqRkGEwpVZbUGAAYQ4bi2AAiYmGYRqcVluQUGAAYQ4pjYNhFzhWW5BQYBZgAwuBFGEOcVdgQFF/CMN5oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBUmAEAWEOaJBhLUlWW2BAUYCRA5D9W1BQUFBQUFBQUFBQUFBWW2AAgGAAYQFnc///////////////////////////FmMV2svqYOAbiIiIiGBAUWAkAWEOvZSTkpGQYSu/VltgQFFgIIGDAwOBUpBgQFKQe/////////////////////////////////////8ZFmAgggGAUXv/////////////////////////////////////g4GDFheDUlBQUFBgQFFhDyeRkGEsQFZbYABgQFGAgwOBhVr0kVBQPYBgAIEUYQ9iV2BAUZFQYB8ZYD89ARaCAWBAUj2CUj1gAGAghAE+YQ9nVltgYJFQW1CRUJFQgWEPeFdgFWEPjVZbgIBgIAGQUYEBkGEPjJGQYSyQVltbYAMLklBQUJSTUFBQUFZbYABgBWf//////////4ERFWEPuVdhD7hhIvxWW1tgQFGQgIJSgGAgAmAgAYIBYEBSgBVhD/JXgWAgAVthD99hIHJWW4FSYCABkGABkAOQgWEP11eQUFtQkFBhEARgAGABYAKHYRbIVluBYACBUYEQYRAYV2EQF2EsvVZbW2AgAmAgAQGBkFJQYRAxYAJgA4CGYRbIVluBYAGBUYEQYRBFV2EQRGEsvVZbW2AgAmAgAQGBkFJQYRBdYARgAYRhFwFWW4FgAoFRgRBhEHFXYRBwYSy9VltbYCACYCABAYGQUlBhEIlgBmABhGEXAVZbgWADgVGBEGEQnVdhEJxhLL1WW1tgIAJgIAEBgZBSUGEQtWAFYASEYRcBVluBYASBUYEQYRDJV2EQyGEsvVZbW2AgAmAgAQGBkFJQYRDcYSDPVluFgWBAAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUGACgFRhESGQYS7IVluAYB8BYCCAkQQCYCABYEBRkIEBYEBSgJKRkIGBUmAgAYKAVGERTZBhLshWW4AVYRGaV4BgHxBhEW9XYQEAgINUBAKDUpFgIAGRYRGaVluCAZGQYABSYCBgACCQW4FUgVKQYAEBkGAgAYCDEWERfVeCkANgHxaCAZFbUFBQUFCBYAABgZBSUGADgFRhEbSQYS7IVluAYB8BYCCAkQQCYCABYEBRkIEBYEBSgJKRkIGBUmAgAYKAVGER4JBhLshWW4AVYRItV4BgHxBhEgJXYQEAgINUBAKDUpFgIAGRYRItVluCAZGQYABSYCBgACCQW4FUgVKQYAEBkGAgAYCDEWESEFeCkANgHxaCAZFbUFBQUFCBYCABgZBSUIGBYOABgZBSUGAEgFRhElCQYS7IVluAYB8BYCCAkQQCYCABYEBRkIEBYEBSgJKRkIGBUmAgAYKAVGESfJBhLshWW4AVYRLJV4BgHxBhEp5XYQEAgINUBAKDUpFgIAGRYRLJVluCAZGQYABSYCBgACCQW4FUgVKQYAEBkGAgAYCDEWESrFeCkANgHxaCAZFbUFBQUFCBYGABgZBSUGAAYRLiiINhFzhWW5BQYBZgAwuBFGETKldgQFF/CMN5oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBUmAEAWETIZBhMcNWW2BAUYCRA5D9W1BQUFBQUFBQVlthEzxhIM9WW2ACgFRhE0mQYS7IVluAYB8BYCCAkQQCYCABYEBRkIEBYEBSgJKRkIGBUmAgAYKAVGETdZBhLshWW4AVYRPCV4BgHxBhE5dXYQEAgINUBAKDUpFgIAGRYRPCVluCAZGQYABSYCBgACCQW4FUgVKQYAEBkGAgAYCDEWETpVeCkANgHxaCAZFbUFBQUFCBYAABgZBSUGADgFRhE9yQYS7IVluAYB8BYCCAkQQCYCABYEBRkIEBYEBSgJKRkIGBUmAgAYKAVGEUCJBhLshWW4AVYRRVV4BgHxBhFCpXYQEAgINUBAKDUpFgIAGRYRRVVluCAZGQYABSYCBgACCQW4FUgVKQYAEBkGAgAYCDEWEUOFeCkANgHxaCAZFbUFBQUFCBYCABgZBSUIGBYEABkHP//////////////////////////xaQgXP//////////////////////////xaBUlBQYASAVGEUp5BhLshWW4BgHwFgIICRBAJgIAFgQFGQgQFgQFKAkpGQgYFSYCABgoBUYRTTkGEuyFZbgBVhFSBXgGAfEGEU9VdhAQCAg1QEAoNSkWAgAZFhFSBWW4IBkZBgAFJgIGAAIJBbgVSBUpBgAQGQYCABgIMRYRUDV4KQA2AfFoIBkVtQUFBQUIFgYAGBkFJQYABhFTmEg2EXOFZbkFBgFmADC4EUYRWBV2BAUX8Iw3mgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIFSYAQBYRV4kGEyVVZbYEBRgJEDkP1bUFBQUFZbYRWPYSDPVluCgWAAAYGQUlCBgWAgAYGQUlCDgWBAAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUGAEgFRhFeaQYS7IVluAYB8BYCCAkQQCYCABYEBRkIEBYEBSgJKRkIGBUmAgAYKAVGEWEpBhLshWW4AVYRZfV4BgHxBhFjRXYQEAgINUBAKDUpFgIAGRYRZfVluCAZGQYABSYCBgACCQW4FUgVKQYAEBkGAgAYCDEWEWQleCkANgHxaCAZFbUFBQUFCBYGABgZBSUGAAYRZ4hoNhFzhWW5BQYBZgAwuBFGEWwFdgQFF/CMN5oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBUmAEAWEWt5BhMudWW2BAUYCRA5D9W1BQUFBQUFZbYRbQYSByVltgQFGAYEABYEBSgGEW5YeHYR0WVluBUmAgAWEW9IWFYR1sVluBUlCQUJSTUFBQUFZbYRcJYSByVltgQFGAYEABYEBSgGEXHYZhH0FWW4FSYCABYRcshYVhH4JWW4FSUJBQk5JQUFBWW2AAgGAAYQFnc///////////////////////////FmN9MFz6YOAbhoZgQFFgJAFhF3GSkZBhNgdWW2BAUWAggYMDA4FSkGBAUpB7/////////////////////////////////////xkWYCCCAYBRe/////////////////////////////////////+DgYMWF4NSUFBQUGBAUWEX25GQYSxAVltgAGBAUYCDA4FgAIZa8ZFQUD2AYACBFGEYGFdgQFGRUGAfGWA/PQEWggFgQFI9glI9YABgIIQBPmEYHVZbYGCRUFtQkVCRUIFhGC5XYBVhGENWW4CAYCABkFGBAZBhGEKRkGEskFZbW2ADC5JQUFCSkVBQVltgAGEYWmEhPlZbYACAYQFnc///////////////////////////FmM8TdMuYOAbh4dgQFFgJAFhGJGSkZBhNjdWW2BAUWAggYMDA4FSkGBAUpB7/////////////////////////////////////xkWYCCCAYBRe/////////////////////////////////////+DgYMWF4NSUFBQUGBAUWEY+5GQYSxAVltgAGBAUYCDA4FgAIZa8ZFQUD2AYACBFGEZOFdgQFGRUGAfGWA/PQEWggFgQFI9glI9YABgIIQBPmEZPVZbYGCRUFtQkVCRUGEZSmEhPlZbgmEZV1dgFYFhGWxWW4GAYCABkFGBAZBhGWuRkGE331ZbW4FgAwuRUICVUIGWUFBQUFBQklCSkFBWW2AAgGAAYQFnc///////////////////////////FmNvw8uvYOAbhoZgQFFgJAFhGb2SkZBhOMFWW2BAUWAggYMDA4FSkGBAUpB7/////////////////////////////////////xkWYCCCAYBRe/////////////////////////////////////+DgYMWF4NSUFBQUGBAUWEaJ5GQYSxAVltgAGBAUYCDA4FgAIZa8ZFQUD2AYACBFGEaZFdgQFGRUGAfGWA/PQEWggFgQFI9glI9YABgIIQBPmEaaVZbYGCRUFtQkVCRUIFhGnpXYBVhGo9WW4CAYCABkFGBAZBhGo6RkGEskFZbW2ADC5JQUFCSkVBQVlthGqRhIM9WW2EarGEgklZbhYFgAAGQYAcLkIFgBwuBUlBQhIFgIAGQc///////////////////////////FpCBc///////////////////////////FoFSUFCDgWBAAZBgBwuQgWAHC4FSUFBgAoBUYRsVkGEuyFZbgGAfAWAggJEEAmAgAWBAUZCBAWBAUoCSkZCBgVJgIAGCgFRhG0GQYS7IVluAFWEbjleAYB8QYRtjV2EBAICDVAQCg1KRYCABkWEbjlZbggGRkGAAUmAgYAAgkFuBVIFSkGABAZBgIAGAgxFhG3FXgpADYB8WggGRW1BQUFBQgmAAAYGQUlBgA4BUYRuokGEuyFZbgGAfAWAggJEEAmAgAWBAUZCBAWBAUoCSkZCBgVJgIAGCgFRhG9SQYS7IVluAFWEcIVeAYB8QYRv2V2EBAICDVAQCg1KRYCABkWEcIVZbggGRkGAAUmAgYAAgkFuBVIFSkGABAZBgIAGAgxFhHARXgpADYB8WggGRW1BQUFBQgmAgAYGQUlCGgmBAAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUIKCYOABgZBSUICCYQEAAYGQUlBgBIBUYRyGkGEuyFZbgGAfAWAggJEEAmAgAWBAUZCBAWBAUoCSkZCBgVJgIAGCgFRhHLKQYS7IVluAFWEc/1eAYB8QYRzUV2EBAICDVAQCg1KRYCABkWEc/1ZbggGRkGAAUmAgYAAgkFuBVIFSkGABAZBgIAGAgxFhHOJXgpADYB8WggGRW1BQUFBQgmBgAYGQUlBQlZRQUFBQUFZbYABhHT2DYAaBERVhHS5XYR0tYTjxVltbgmEgXpCRkGP/////FlZbkFBhHWSCYAaBERVhHVVXYR1UYTjxVltbgmEgXpCRkGP/////FlZbkFCSkVBQVlthHXRhIT5WW2AAYASBERVhHYhXYR2HYTjxVltbg2AEgREVYR2bV2EdmmE48VZbWwNhHbZXYAGBYAABkBUVkIEVFYFSUFBhHztWW2ABYASBERVhHcpXYR3JYTjxVltbg2AEgREVYR3dV2Ed3GE48VZbWwNhHj9XYACAVJBhAQAKkARz//////////////////////////8WgWAgAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUGEfOlZbYAJgBIERFWEeU1dhHlJhOPFWW1uDYASBERVhHmZXYR5lYTjxVltbA2EeeVeBgWBAAYGQUlBhHzlWW2ADYASBERVhHo1XYR6MYTjxVltbg2AEgREVYR6gV2Een2E48VZbWwNhHrNXgYFgYAGBkFJQYR84VltgBICBERVhHsZXYR7FYTjxVltbg2AEgREVYR7ZV2Ee2GE48VZbWwNhHzdXYACAVJBhAQAKkARz//////////////////////////8WgWCAAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUFtbW1tbkpFQUFZbYABgAWAAg2AGgREVYR9aV2EfWWE48VZbW2AGgREVYR9sV2Efa2E48VZbW4FSYCABkIFSYCABYAAgVJBQkZBQVlthH4phIT5WW2ABYASBERVhH55XYR+dYTjxVltbg2AEgREVYR+xV2EfsGE48VZbWwNhH/NXgYFgIAGQc///////////////////////////FpCBc///////////////////////////FoFSUFBhIFhWW2AEgIERFWEgBldhIAVhOPFWW1uDYASBERVhIBlXYSAYYTjxVltbA2EgV1eBgWCAAZBz//////////////////////////8WkIFz//////////////////////////8WgVJQUFtbkpFQUFZbYACBYP8WYAGQG4MXkFCSkVBQVltgQFGAYEABYEBSgGAAgVJgIAFhIIxhIT5WW4FSUJBWW2BAUYBgYAFgQFKAYABgBwuBUmAgAWAAc///////////////////////////FoFSYCABYABgBwuBUlCQVltgQFGAYQEgAWBAUoBgYIFSYCABYGCBUmAgAWAAc///////////////////////////FoFSYCABYGCBUmAgAWAAFRWBUmAgAWAAYAcLgVJgIAFgABUVgVJgIAFgYIFSYCABYSE4YSCSVluBUlCQVltgQFGAYKABYEBSgGAAFRWBUmAgAWAAc///////////////////////////FoFSYCABYGCBUmAgAWBggVJgIAFgAHP//////////////////////////xaBUlCQVltgAGBAUZBQkFZbYACA/VtgAID9W2AAc///////////////////////////ghaQUJGQUFZbYABhIdqCYSGvVluQUJGQUFZbYSHqgWEhz1ZbgRRhIfVXYACA/VtQVltgAIE1kFBhIgeBYSHhVluSkVBQVltgAIGQUJGQUFZbYSIggWEiDVZbgRRhIitXYACA/VtQVltgAIE1kFBhIj2BYSIXVluSkVBQVltgAIBgAIBggIWHAxIVYSJdV2EiXGEhpVZbW2AAYSJrh4KIAWEh+FZblFBQYCBhInyHgogBYSH4VluTUFBgQGEijYeCiAFhIfhWW5JQUGBgYSKeh4KIAWEiLlZbkVBQkpWRlFCSUFZbYACBYAcLkFCRkFBWW2EiwIFhIqpWW4JSUFBWW2AAYCCCAZBQYSLbYACDAYRhIrdWW5KRUFBWW2AAgP1bYACA/VtgAGAfGWAfgwEWkFCRkFBWW39OSHtxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAUmBBYARSYCRgAP1bYSM0gmEi61ZbgQGBgRBn//////////+CERcVYSNTV2EjUmEi/FZbW4BgQFJQUFBWW2AAYSNmYSGbVluQUGEjcoKCYSMrVluRkFBWW2AAZ///////////ghEVYSOSV2EjkWEi/FZbW2Ejm4JhIutWW5BQYCCBAZBQkZBQVluCgYM3YACDgwFSUFBQVltgAGEjymEjxYRhI3dWW2EjXFZbkFCCgVJgIIEBhISEAREVYSPmV2Ej5WEi5lZbW2Ej8YSChWEjqFZbUJOSUFBQVltgAIJgH4MBEmEkDldhJA1hIuFWW1uBNWEkHoSCYCCGAWEjt1ZbkVBQkpFQUFZbYSQwgWEiqlZbgRRhJDtXYACA/VtQVltgAIE1kFBhJE2BYSQnVluSkVBQVltgAIBgAIBgAIBgAGDgiIoDEhVhJHJXYSRxYSGlVltbYABhJICKgosBYSH4VluXUFBgIGEkkYqCiwFhIfhWW5ZQUGBAiAE1Z///////////gREVYSSyV2EksWEhqlZbW2EkvoqCiwFhI/lWW5VQUGBgiAE1Z///////////gREVYSTfV2Ek3mEhqlZbW2Ek64qCiwFhI/lWW5RQUGCAYST8ioKLAWEh+FZbk1BQYKBhJQ2KgosBYSH4VluSUFBgwGElHoqCiwFhJD5WW5FQUJKVmJGUl1CSlVBWW2AAgGBAg4UDEhVhJURXYSVDYSGlVltbYABhJVKFgoYBYSH4VluSUFBgIGElY4WChgFhIi5WW5FQUJJQkpBQVltgAIEVFZBQkZBQVlthJYKBYSVtVluCUlBQVlthJZGBYSHPVluCUlBQVltgAIFRkFCRkFBWW2AAgoJSYCCCAZBQkpFQUFZbYABbg4EQFWEl0VeAggFRgYQBUmAggQGQUGEltlZbYACEhAFSUFBQUFZbYABhJeiCYSWXVlthJfKBhWElolZbk1BhJgKBhWAghgFhJbNWW2EmC4FhIutWW4QBkVBQkpFQUFZbYABgoIMBYACDAVFhJi5gAIYBgmEleVZbUGAggwFRYSZBYCCGAYJhJYhWW1BgQIMBUYSCA2BAhgFSYSZZgoJhJd1WW5FQUGBggwFRhIIDYGCGAVJhJnOCgmEl3VZbkVBQYICDAVFhJohggIYBgmEliFZbUICRUFCSkVBQVltgAGAgggGQUIGBA2AAgwFSYSatgYRhJhZWW5BQkpFQUFZbYACAYECDhQMSFWEmzFdhJsthIaVWW1tgAGEm2oWChgFhIfhWW5JQUGAggwE1Z///////////gREVYSb7V2Em+mEhqlZbW2EnB4WChgFhI/lWW5FQUJJQkpBQVltgAIBgAIBggIWHAxIVYScrV2EnKmEhpVZbW2AAYSc5h4KIAWEh+FZblFBQYCCFATVn//////////+BERVhJ1pXYSdZYSGqVltbYSdmh4KIAWEj+VZbk1BQYECFATVn//////////+BERVhJ4dXYSeGYSGqVltbYSeTh4KIAWEj+VZbklBQYGBhJ6SHgogBYSH4VluRUFCSlZGUUJJQVltgAIBgAGBghIYDEhVhJ8lXYSfIYSGlVltbYABhJ9eGgocBYSH4VluTUFBgIGEn6IaChwFhIfhWW5JQUGBAYSf5hoKHAWEkPlZbkVBQklCSUJJWW2AAZ///////////ghEVYSgeV2EoHWEi/FZbW2EoJ4JhIutWW5BQYCCBAZBQkZBQVltgAGEoR2EoQoRhKANWW2EjXFZbkFCCgVJgIIEBhISEAREVYShjV2EoYmEi5lZbW2EoboSChWEjqFZbUJOSUFBQVltgAIJgH4MBEmEoi1dhKIphIuFWW1uBNWEom4SCYCCGAWEoNFZbkVBQkpFQUFZbYACAYACAYACAYACAYACAYQFAi40DEhVhKMhXYSjHYSGlVltbYABhKNaNgo4BYSH4VluaUFBgIGEo542CjgFhIfhWW5lQUGBAiwE1Z///////////gREVYSkIV2EpB2EhqlZbW2EpFI2CjgFhI/lWW5hQUGBgiwE1Z///////////gREVYSk1V2EpNGEhqlZbW2EpQY2CjgFhI/lWW5dQUGCAYSlSjYKOAWEh+FZbllBQYKBhKWONgo4BYSH4VluVUFBgwGEpdI2CjgFhJD5WW5RQUGDgiwE1Z///////////gREVYSmVV2EplGEhqlZbW2EpoY2CjgFhKHZWW5NQUGEBAIsBNWf//////////4ERFWEpw1dhKcJhIapWW1thKc+Ngo4BYSh2VluSUFBhASCLATVn//////////+BERVhKfFXYSnwYSGqVltbYSn9jYKOAWEodlZbkVBQkpWYm5GUl5pQkpWYUFZbYACAYACAYABgoIaIAxIVYSorV2EqKmEhpVZbW2AAYSo5iIKJAWEh+FZblVBQYCBhKkqIgokBYSH4VluUUFBgQIYBNWf//////////4ERFWEqa1dhKmphIapWW1thKneIgokBYSP5VluTUFBgYIYBNWf//////////4ERFWEqmFdhKpdhIapWW1thKqSIgokBYSP5VluSUFBggGEqtYiCiQFhIfhWW5FQUJKVUJKVkJNQVltgAIBgQIOFAxIVYSrZV2Eq2GEhpVZbW2AAYSrnhYKGAWEh+FZbklBQYCBhKviFgoYBYSH4VluRUFCSUJKQUFZbYACAYACAYICFhwMSFWErHFdhKxthIaVWW1tgAGErKoeCiAFhIfhWW5RQUGAgYSs7h4KIAWEh+FZbk1BQYECFATVn//////////+BERVhK1xXYStbYSGqVltbYStoh4KIAWEodlZbklBQYGCFATVn//////////+BERVhK4lXYSuIYSGqVltbYSuVh4KIAWEodlZbkVBQkpWRlFCSUFZbYSuqgWEhz1ZbglJQUFZbYSu5gWEiDVZbglJQUFZbYABggIIBkFBhK9RgAIMBh2EroVZbYSvhYCCDAYZhK6FWW2Er7mBAgwGFYSuhVlthK/tgYIMBhGErsFZblZRQUFBQUFZbYACBkFCSkVBQVltgAGEsGoJhJZdWW2EsJIGFYSwEVluTUGEsNIGFYCCGAWEls1ZbgIQBkVBQkpFQUFZbYABhLEyChGEsD1ZbkVCBkFCSkVBQVltgAIFgAwuQUJGQUFZbYSxtgWEsV1ZbgRRhLHhXYACA/VtQVltgAIFRkFBhLIqBYSxkVluSkVBQVltgAGAggoQDEhVhLKZXYSylYSGlVltbYABhLLSEgoUBYSx7VluRUFCSkVBQVlt/Tkh7cQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAFJgMmAEUmAkYAD9W2AAgoJSYCCCAZBQkpFQUFZbf1VwZGF0ZSBvZiB0b2tlbkluZm8gZmFpbGVkIQAAAAAAYACCAVJQVltgAGEtM2Abg2Es7FZbkVBhLT6CYSz9VltgIIIBkFCRkFBWW2AAYCCCAZBQgYEDYACDAVJhLWKBYS0mVluQUJGQUFZbYACCglJgIIIBkFCSkVBQVltgAGEthYJhJZdWW2Etj4GFYS1pVluTUGEtn4GFYCCGAWEls1ZbYS2ogWEi61ZbhAGRUFCSkVBQVltgAGBAggGQUGEtyGAAgwGFYSuhVluBgQNgIIMBUmEt2oGEYS16VluQUJOSUFBQVlthLeyBYSVtVluCUlBQVltgAGBAggGQUGEuB2AAgwGFYS3jVluBgQNgIIMBUmEuGYGEYS16VluQUJOSUFBQVlt/VXBkYXRlIG9mIHRva2VuIGtleXMgZmFpbGVkIQAAAABgAIIBUlBWW2AAYS5YYByDYSzsVluRUGEuY4JhLiJWW2AgggGQUJGQUFZbYABgIIIBkFCBgQNgAIMBUmEuh4FhLktWW5BQkZBQVltgAIFRkFCRkFBWW39OSHtxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAUmAiYARSYCRgAP1bYABgAoIEkFBgAYIWgGEu4Fdgf4IWkVBbYCCCEIEDYS7zV2Eu8mEumVZbW1CRkFBWW2AAgZBQgWAAUmAgYAAgkFCRkFBWW2AAYCBgH4MBBJBQkZBQVltgAIKCG5BQkpFQUFZbYABgCIMCYS9bf///////////////////////////////////////////gmEvHlZbYS9lhoNhLx5WW5VQgBmEFpNQgIYWhBeSUFBQk5JQUFBWW2AAgZBQkZBQVltgAGEvomEvnWEvmIRhIg1WW2EvfVZbYSINVluQUJGQUFZbYACBkFCRkFBWW2EvvINhL4dWW2Ev0GEvyIJhL6lWW4SEVGEvK1ZbglVQUFBQVltgAJBWW2Ev5WEv2FZbYS/wgYSEYS+zVltQUFBWW1uBgRAVYTAUV2EwCWAAgmEv3VZbYAGBAZBQYS/2VltQUFZbYB+CERVhMFlXYTAqgWEu+VZbYTAzhGEvDlZbgQFgIIUQFWEwQleBkFBbYTBWYTBOhWEvDlZbgwGCYS/1VltQUFtQUFBWW2AAgoIckFCSkVBQVltgAGEwfGAAGYRgCAJhMF5WWxmAgxaRUFCSkVBQVltgAGEwlYODYTBrVluRUIJgAgKCF5BQkpFQUFZbYTCugmEujlZbZ///////////gREVYTDHV2EwxmEi/FZbW2Ew0YJUYS7IVlthMNyCgoVhMBhWW2AAYCCQUGAfgxFgAYEUYTEPV2AAhBVhMP1XgocBUZBQW2ExB4WCYTCJVluGVVBhMW9WW2AfGYQWYTEdhmEu+VZbYABbgoEQFWExRVeEiQFRglVgAYIBkVBgIIUBlFBgIIEBkFBhMSBWW4aDEBVhMWJXhIkBUWExXmAfiRaCYTBrVluDVVBbYAFgAogCAYhVUFBQW1BQUFBQUFZbf1VwZGF0ZSBvZiB0b2tlbkluZm8ga2V5cyBmYWlsZWQhYACCAVJQVltgAGExrWAgg2Es7FZbkVBhMbiCYTF3VltgIIIBkFCRkFBWW2AAYCCCAZBQgYEDYACDAVJhMdyBYTGgVluQUJGQUFZbf1VwZGF0ZSBvZiB0b2tlbkluZm8udHJlYXN1cnkgZmFpYACCAVJ/bGVkIQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgIIIBUlBWW2AAYTI/YCSDYSzsVluRUGEySoJhMeNWW2BAggGQUJGQUFZbYABgIIIBkFCBgQNgAIMBUmEyboFhMjJWW5BQkZBQVlt/VXBkYXRlIG9mIHRva2VuSW5mbyBuYW1lIGFuZCBzeW1gAIIBUn9ib2wgZmFpbGVkIQAAAAAAAAAAAAAAAAAAAAAAAAAAAGAgggFSUFZbYABhMtFgK4NhLOxWW5FQYTLcgmEydVZbYECCAZBQkZBQVltgAGAgggGQUIGBA2AAgwFSYTMAgWEyxFZbkFCRkFBWW2AAgoJSYCCCAZBQkpFQUFZbYABhMyOCYS6OVlthMy2BhWEzB1Zbk1BhMz2BhWAghgFhJbNWW2EzRoFhIutWW4QBkVBQkpFQUFZbYTNagWEiqlZbglJQUFZbYACBUZBQkZBQVltgAIKCUmAgggGQUJKRUFBWW2AAgZBQYCCCAZBQkZBQVlthM5WBYSINVluCUlBQVltgAGCggwFgAIMBUWEzs2AAhgGCYSV5VltQYCCDAVFhM8ZgIIYBgmEliFZbUGBAgwFRhIIDYECGAVJhM96CgmEl3VZbkVBQYGCDAVGEggNgYIYBUmEz+IKCYSXdVluRUFBggIMBUWE0DWCAhgGCYSWIVltQgJFQUJKRUFBWW2AAYECDAWAAgwFRYTQwYACGAYJhM4xWW1BgIIMBUYSCA2AghgFSYTRIgoJhM5tWW5FQUICRUFCSkVBQVltgAGE0YYODYTQYVluQUJKRUFBWW2AAYCCCAZBQkZBQVltgAGE0gYJhM2BWW2E0i4GFYTNrVluTUINgIIIChQFhNJ2FYTN8VluAYABbhYEQFWE02VeEhAOJUoFRYTS6hYJhNFVWW5RQYTTFg2E0aVZbklBgIIoBmVBQYAGBAZBQYTShVltQgpdQh5VQUFBQUFCSkVBQVltgYIIBYACCAVFhNQFgAIUBgmEzUVZbUGAgggFRYTUUYCCFAYJhJYhWW1BgQIIBUWE1J2BAhQGCYTNRVltQUFBQVltgAGEBYIMBYACDAVGEggNgAIYBUmE1S4KCYTMYVluRUFBgIIMBUYSCA2AghgFSYTVlgoJhMxhWW5FQUGBAgwFRYTV6YECGAYJhJYhWW1BgYIMBUYSCA2BghgFSYTWSgoJhMxhWW5FQUGCAgwFRYTWnYICGAYJhJXlWW1BgoIMBUWE1umCghgGCYTNRVltQYMCDAVFhNc1gwIYBgmEleVZbUGDggwFRhIIDYOCGAVJhNeWCgmE0dlZbkVBQYQEAgwFRYTX8YQEAhgGCYTTrVltQgJFQUJKRUFBWW2AAYECCAZBQYTYcYACDAYVhK6FWW4GBA2AggwFSYTYugYRhNS1WW5BQk5JQUFBWW2AAYECCAZBQYTZMYACDAYVhK6FWW2E2WWAggwGEYSuwVluTklBQUFZbYACA/VtgAID9W2E2c4FhJW1WW4EUYTZ+V2AAgP1bUFZbYACBUZBQYTaQgWE2alZbkpFQUFZbYACBUZBQYTalgWEh4VZbkpFQUFZbYABhNr5hNrmEYSN3VlthI1xWW5BQgoFSYCCBAYSEhAERFWE22ldhNtlhIuZWW1thNuWEgoVhJbNWW1CTklBQUFZbYACCYB+DARJhNwJXYTcBYSLhVltbgVFhNxKEgmAghgFhNqtWW5FQUJKRUFBWW2AAYKCChAMSFWE3MVdhNzBhNmBWW1thNztgoGEjXFZbkFBgAGE3S4SChQFhNoFWW2AAgwFSUGAgYTdfhIKFAWE2llZbYCCDAVJQYECCAVFn//////////+BERVhN4NXYTeCYTZlVltbYTePhIKFAWE27VZbYECDAVJQYGCCAVFn//////////+BERVhN7NXYTeyYTZlVltbYTe/hIKFAWE27VZbYGCDAVJQYIBhN9OEgoUBYTaWVltggIMBUlCSkVBQVltgAIBgQIOFAxIVYTf2V2E39WEhpVZbW2AAYTgEhYKGAWEse1ZbklBQYCCDAVFn//////////+BERVhOCVXYTgkYSGqVltbYTgxhYKGAWE3G1ZbkVBQklCSkFBWW2AAgoJSYCCCAZBQkpFQUFZbYABhOFeCYTNgVlthOGGBhWE4O1Zbk1CDYCCCAoUBYThzhWEzfFZbgGAAW4WBEBVhOK9XhIQDiVKBUWE4kIWCYTRVVluUUGE4m4NhNGlWW5JQYCCKAZlQUGABgQGQUGE4d1ZbUIKXUIeVUFBQUFBQkpFQUFZbYABgQIIBkFBhONZgAIMBhWEroVZbgYEDYCCDAVJhOOiBhGE4TFZbkFCTklBQUFZbf05Ie3EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYABSYCFgBFJgJGAA/f6iZGlwZnNYIhIg7QFwC+WSFuwDFluKIf2VzM+UdsVrk6tZl4cqjf3sumRkc29sY0MACBIAMyKAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAogKjDAToDGPsHShYKFAAAAAAAAAAAAAAAAAAAAAAAAAP7cgcKAxj7BxABUhgKCgoCGAIQ/6/W2AEKCgoCGGIQgLDW2AE="},{"b64Body":"Cg8KCQjU4J6tBhDHCBICGAISAhgDGP7v5OgCIgIIeDIgw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo7qAUwKBlRva2VuRBIIRk5XRFZLRUQqAxj4B1IiEiC87mXWk5k3PTCiEalFU1Kg6JWU2mXOG0gaV32aec+q+GoMCJCv+bAGEOjQvNABiAEB","b64Record":"CiUIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkD1IDGPwHEjAO5A2vZmSCkMLTCXPoKmXfgWiUQVXGApS4eODfjNj48fDkw7AnQJf9K2/YXTmX3xgaDAiQ4Z6tBhC7mrjnASIPCgkI1OCerQYQxwgSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIAcgoKAxj8BxIDGPgH"},{"b64Body":"Cg8KCQjU4J6tBhDNCBICGAISAhgDGNPtlwgiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjqoCCwoDGPwHGgRuZnQ1","b64Record":"CiUIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkD1gBcgEBEjDdPKbLVBT98SYB2SLt/ZfmncwaF1C7xjdc3nvG4JOQXkoJeQmp6rSxGWh727sbXKoaDAiQ4Z6tBhCrtIHMAyIPCgkI1OCerQYQzQgSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjlIAWhIKAxj8BxoLCgIYABIDGPgHGAE="},{"b64Body":"Cg8KCQjV4J6tBhDVCBICGAISAhgDGOC2iyAiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjsICCgoDGPkHEgMY/Ac=","b64Record":"CiAIFiocCgwIARAMGgYIgK6ZpA8SDAgBEA8aBgiArpmkDxIwbHgOwAZyjtj2kbWXwkavPYOTCGbmo0PbdSJpy3oa1TsrSi8NiDlOQcWPHjZ2NkIrGgwIkeGerQYQo4u/gwIiDwoJCNXgnq0GENUIEgIYAiogw4PCrsOCwrfDg8K5dEY4w4LCrkrDg8KLw4PCkMODwo5SAA=="},{"b64Body":"Cg8KCQjW4J6tBhDbCBICGAISAhgDGOCssQMiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjjpPCgMY+wcQoI0GIkRBKbfbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWQ==","b64Record":"CiUIISIDGPsHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjAR/Hn2DQqbvJSwyfp56F6HSyOWXbY9u3d4UATcK3Tn27lt7bJRjxSp39mqpcQamaIaCwiS4Z6tBhDT0dExIg8KCQjW4J6tBhDbCBICGAIqIMODwq7DgsK3w4PCuXRGOMOCwq5Kw4PCi8ODwpDDg8KOMLnwrQM6CBoCMHgo/4YGUhYKCQoCGAIQ8eDbBgoJCgIYYhDy4NsG"},{"b64Body":"Cg8KCQjW4J6tBhDdCBICGAISAhgDGOCssQMiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjjpPCgMY+wcQoI0GIkRBKbfbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEA==","b64Record":"CiUIISIDGPsHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjCe1yFoCzbscPN8gU2iBtT/GFPBZijWsJVISHn2vq1fYrKB6sqWFRtMkfMHTrZH7H8aDAiS4Z6tBhCLqKWzAiIPCgkI1uCerQYQ3QgSAhgCKiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjjCA19oCOggaAjB4KIDxBFIWCgkKAhgCEP+ttQUKCQoCGGIQgK61BQ=="},{"b64Body":"ChEKCQjW4J6tBhDdCBICGAIgATpNCgMY5wIQASJEPE3TLgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA=","b64Record":"CgMIpwESMGrqksQXzTR/Fhmxq7BCtkHdvdKgL+j8RpXQhJ/E1x8albVAivE1agW3+tnBg3CHSxoMCJLhnq0GEIyopbMCIhEKCQjW4J6tBhDdCBICGAIgATpACgMY5wISIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACnGhBJTlZBTElEX1RPS0VOX0lEKGRqAxj7B1IAegwIkuGerQYQi6ilswI="},{"b64Body":"Cg8KCQjX4J6tBhDfCBICGAISAhgDGOCssQMiAgh4MiDDg8Kuw4LCt8ODwrl0RjjDgsKuSsODwovDg8KQw4PCjjpPCgMY+wcQoI0GIkRBKbfbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ==","b64Record":"CiUIISIDGPsHKhwKDAgBEAwaBgiArpmkDxIMCAEQDxoGCICumaQPEjDkg1CqqC0nQj4fS7qAxKzCvhuSm3e3E69rYzLQPF313VBK7vU+GuWPsEkoK2inMcwaCwiT4Z6tBhDDi9o7Ig8KCQjX4J6tBhDfCBICGAIqIMODwq7DgsK3w4PCuXRGOMOCwq5Kw4PCi8ODwpDDg8KOMIDX2gI6CBoCMHgogPEEUhYKCQoCGAIQ/621BQoJCgIYYhCArrUF"},{"b64Body":"ChEKCQjX4J6tBhDfCBICGAIgATpNCgMY5wIQASJEPE3TLgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE=","b64Record":"CgIILBIwmNzr1+AONPIp6PWeOhhofUvHxQQvvLb2e4j55Oug3pMP+ZBfkcHt3lfbZtrGnYkgGgsIk+GerQYQxIvaOyIRCgkI1+CerQYQ3wgSAhgCIAE6QAoDGOcCEiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALBoQS0VZX05PVF9QUk9WSURFRChkagMY+wdSAHoLCJPhnq0GEMOL2js="}]}}} \ No newline at end of file diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpecSetup.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpecSetup.java index 0e9c5e6bc2e7..25b823dfbed0 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpecSetup.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpecSetup.java @@ -318,6 +318,16 @@ public boolean autoSnapshotManagement() { return props.getBoolean("recordStream.autoSnapshotManagement"); } + /** + * Returns whether a {@link HapiSpec} doing automatic snapshot management should + * override an existing snapshot. + * + * @return whether an auto-snapshot managing {@link HapiSpec} should override an existing snapshot + */ + public boolean overrideExistingSnapshot() { + return props.getBoolean("recordStream.overrideExistingSnapshot"); + } + /** * Returns the record stream source for the {@link HapiSpec} to use when automatically taking snapshots * with {@code recordStream.autoSnapshotManagement=true}. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/domain/ParsedItem.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/domain/ParsedItem.java index d21eed81d5aa..8d3a3b5a3087 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/domain/ParsedItem.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/domain/ParsedItem.java @@ -21,6 +21,7 @@ import com.google.protobuf.InvalidProtocolBufferException; import com.hedera.services.stream.proto.RecordStreamItem; import com.hederahashgraph.api.proto.java.FileID; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.SignedTransaction; import com.hederahashgraph.api.proto.java.TransactionBody; import com.hederahashgraph.api.proto.java.TransactionRecord; @@ -36,6 +37,10 @@ public record ParsedItem(TransactionBody itemBody, TransactionRecord itemRecord) private static final FileID PROPERTIES_FILE_ID = FileID.newBuilder().setFileNum(121).build(); + public ResponseCodeEnum status() { + return itemRecord.getReceipt().getStatus(); + } + public static ParsedItem parse(final RecordStreamItem item) throws InvalidProtocolBufferException { final var txn = item.getTransaction(); final TransactionBody body; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/records/AutoSnapshotModeOp.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/records/AutoSnapshotModeOp.java index 24c799683c0c..8c19fd965ed2 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/records/AutoSnapshotModeOp.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/records/AutoSnapshotModeOp.java @@ -64,7 +64,7 @@ public AutoSnapshotModeOp( @Override protected boolean submitOp(@NonNull final HapiSpec spec) throws Throwable { final var maybeSnapshot = SnapshotModeOp.maybeLoadSnapshotFor(spec); - if (maybeSnapshot.isPresent()) { + if (maybeSnapshot.isPresent() && !spec.setup().overrideExistingSnapshot()) { final var snapshotMode = (autoMatchSource == MONO_SERVICE) ? SnapshotMode.FUZZY_MATCH_AGAINST_MONO_STREAMS : SnapshotMode.FUZZY_MATCH_AGAINST_HAPI_TEST_STREAMS; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/records/SnapshotModeOp.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/records/SnapshotModeOp.java index af805f055aa5..1bebb81d03f1 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/records/SnapshotModeOp.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/records/SnapshotModeOp.java @@ -53,6 +53,7 @@ import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.ContractID; import com.hederahashgraph.api.proto.java.FileID; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.ScheduleID; import com.hederahashgraph.api.proto.java.TokenID; import com.hederahashgraph.api.proto.java.TopicID; @@ -184,8 +185,7 @@ public class SnapshotModeOp extends UtilOp implements SnapshotOp { public static void main(String... args) throws IOException { // Helper to review the snapshot saved for a particular HapiSuite-HapiSpec combination - final var snapshotFileMeta = - new SnapshotFileMeta("HelloWorldEthereum", "createWithSelfDestructInConstructorHasSaneRecord"); + final var snapshotFileMeta = new SnapshotFileMeta("ERCPrecompile", "getErc721TokenURIFromErc20TokenFails"); final var maybeSnapshot = suiteSnapshotsFrom( resourceLocOf(PROJECT_ROOT_SNAPSHOT_RESOURCES_LOC, snapshotFileMeta.suiteName())) .flatMap( @@ -256,8 +256,9 @@ static Optional maybeLoadSnapshotFor(@NonNull final HapiSpec spe @Override public boolean hasWorkToDo() { - // We leave the spec name null in submitOp() if we are running against a target network that - // doesn't match the SnapshotMode of this operation; or if the HapiSpec is non-deterministic + // We leave the snapshot file metadata null in submitOp() if we are running against a + // target network that doesn't match the SnapshotMode of this operation; or if the + // HapiSpec is non-deterministic return snapshotFileMeta != null; } @@ -290,6 +291,12 @@ public void finishLifecycle(@NonNull final HapiSpec spec) { .toList(); // We only want to snapshot or fuzzy-match the records that come after the placeholder creation boolean placeholderFound = false; + // For statuses that only mono-service rejects at ingest, we need to skip fuzzy-matching; + // unless there is some special case in the spec where mono-service will still use them + // (primarily because they appear in a contract operation's child records) + final Set statusesToIgnore = !matchModes.contains(EXPECT_STREAMLINED_INGEST_RECORDS) + ? spec.setup().streamlinedIngestChecks() + : EnumSet.noneOf(ResponseCodeEnum.class); for (final var item : allItems) { final var parsedItem = ParsedItem.parse(item); if (parsedItem.isPropertyOverride()) { @@ -301,14 +308,7 @@ public void finishLifecycle(@NonNull final HapiSpec spec) { // We cannot ever expect to match node stake update export sequencing continue; } - if (spec.setup() - .streamlinedIngestChecks() - .contains(parsedItem.itemRecord().getReceipt().getStatus()) - && !matchModes.contains(EXPECT_STREAMLINED_INGEST_RECORDS)) { - // There are no records written in mono-service when a transaction fails in ingest. - // But in modular service we write them. While validating fuzzy records, we always skip the records - // with status in spec.streamlinedIngestChecks. But for some error codes like INVALID_ACCOUNT_ID, - // which are thrown in both ingest and handle, we need to validate the records. + if (statusesToIgnore.contains(parsedItem.status())) { continue; } if (!placeholderFound) { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ApproveAllowanceSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ApproveAllowanceSuite.java index e5ce69926f66..7874b7557b62 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ApproveAllowanceSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ApproveAllowanceSuite.java @@ -248,6 +248,7 @@ final HapiSpec htsTokenApproveToInnerContract() { final HapiSpec htsTokenAllowance() { final var theSpender = SPENDER; final var allowanceTxn = ALLOWANCE_TX; + final var notAnAddress = new byte[20]; return defaultHapiSpec("htsTokenAllowance") .given( @@ -286,7 +287,20 @@ final HapiSpec htsTokenAllowance() { asAddress(spec.registry().getAccountID(theSpender)))) .payingWith(OWNER) .via(allowanceTxn) - .hasKnownStatus(SUCCESS)))) + .hasKnownStatus(SUCCESS) + // ,contractCall( + // HTS_APPROVE_ALLOWANCE_CONTRACT, + // "htsAllowance", + // asHeadlongAddress( + // + // asAddress(spec.registry().getTokenID(FUNGIBLE_TOKEN))), + // HapiParserUtil.asHeadlongAddress(notAnAddress), + // asHeadlongAddress( + // + // asAddress(spec.registry().getAccountID(theSpender)))) + // .payingWith(OWNER) + // .via("fakeAddressAllowance") + ))) .then( getTxnRecord(allowanceTxn).andAllChildRecords(), childRecordsCheck( @@ -298,7 +312,20 @@ final HapiSpec htsTokenAllowance() { .contractCallResult(htsPrecompileResult() .forFunction(FunctionType.HAPI_ALLOWANCE) .withStatus(SUCCESS) - .withAllowance(2))))); + .withAllowance(2)))) + // ,childRecordsCheck( + // "fakeAddressAllowance", + // SUCCESS, + // recordWith() + // .status(INVALID_ALLOWANCE_OWNER_ID) + // .contractCallResult(resultWith() + // .contractCallResult(htsPrecompileResult() + // + // .forFunction(FunctionType.HAPI_ALLOWANCE) + // + // .withStatus(INVALID_ALLOWANCE_OWNER_ID) + // .withAllowance(0)))) + ); } @HapiTest @@ -366,6 +393,7 @@ final HapiSpec htsTokenApprove() { final HapiSpec hapiNftIsApprovedForAll() { final var notApprovedTxn = "notApprovedTxn"; final var approvedForAllTxn = "approvedForAllTxn"; + final var notAnAddress = new byte[20]; return defaultHapiSpec("hapiNftIsApprovedForAll") .given( @@ -428,7 +456,22 @@ final HapiSpec hapiNftIsApprovedForAll() { .payingWith(OWNER) .via(notApprovedTxn) .hasKnownStatus(SUCCESS) - .gas(GAS_TO_OFFER)))) + .gas(GAS_TO_OFFER) + // ,contractCall( + // HTS_APPROVE_ALLOWANCE_CONTRACT, + // "htsIsApprovedForAll", + // asHeadlongAddress( + // + // asAddress(spec.registry().getTokenID(NON_FUNGIBLE_TOKEN))), + // HapiParserUtil.asHeadlongAddress(notAnAddress), + // asHeadlongAddress( + // asAddress(spec.registry().getAccountID(ACCOUNT)))) + // .payingWith(OWNER) + // .via("fakeAddressIsApprovedForAll") + // .hasKnownStatus(SUCCESS) + // .gas(GAS_TO_OFFER) + + ))) .then( childRecordsCheck( approvedForAllTxn, @@ -447,7 +490,19 @@ final HapiSpec hapiNftIsApprovedForAll() { .contractCallResult(resultWith() .contractCallResult(htsPrecompileResult() .forFunction(FunctionType.HAPI_IS_APPROVED_FOR_ALL) - .withIsApprovedForAll(SUCCESS, false))))); + .withIsApprovedForAll(SUCCESS, false)))) + // ,childRecordsCheck( + // "fakeAddressIsApprovedForAll", + // SUCCESS, + // recordWith() + // .status(SUCCESS) + // .contractCallResult(resultWith() + // .contractCallResult(htsPrecompileResult() + // + // .forFunction(FunctionType.HAPI_IS_APPROVED_FOR_ALL) + // .withIsApprovedForAll(SUCCESS, + // false)))) + ); } @HapiTest @@ -455,6 +510,7 @@ final HapiSpec hapiNftGetApproved() { final var theSpender = SPENDER; final var theSpender2 = "spender2"; final var allowanceTxn = ALLOWANCE_TX; + final var notAnAddress = new byte[20]; return defaultHapiSpec("hapiNftGetApproved") .given( @@ -492,7 +548,15 @@ final HapiSpec hapiNftGetApproved() { BigInteger.ONE) .payingWith(OWNER) .via(allowanceTxn) - .hasKnownStatus(SUCCESS)))) + .hasKnownStatus(SUCCESS) + // ,contractCall( + // HTS_APPROVE_ALLOWANCE_CONTRACT, + // "htsGetApproved", + // HapiParserUtil.asHeadlongAddress(notAnAddress), + // BigInteger.ONE) + // .via("fakeAddressGetApproved") + // .payingWith(OWNER) + ))) .then(withOpContext((spec, opLog) -> allRunFor( spec, childRecordsCheck( @@ -507,7 +571,23 @@ final HapiSpec hapiNftGetApproved() { SUCCESS, asAddress( spec.registry() - .getAccountID(theSpender))))))))); + .getAccountID(theSpender)))))) + // ,childRecordsCheck( + // "fakeAddressGetApproved", + // SUCCESS, + // recordWith() + // .status(INVALID_TOKEN_NFT_SERIAL_NUMBER) + // .contractCallResult(resultWith() + // .contractCallResult(htsPrecompileResult() + // + // .forFunction(FunctionType.HAPI_GET_APPROVED) + // .withApproved( + // + // INVALID_TOKEN_NFT_SERIAL_NUMBER, + // + // asAddress(AccountID.getDefaultInstance()) + // )))) + ))); } @HapiTest diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DefaultTokenStatusSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DefaultTokenStatusSuite.java index 0286e60461af..c1f67bdc1a3d 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DefaultTokenStatusSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DefaultTokenStatusSuite.java @@ -88,6 +88,7 @@ public boolean canRunConcurrent() { @HapiTest final HapiSpec getTokenDefaultFreezeStatus() { final AtomicReference vanillaTokenID = new AtomicReference<>(); + final var notAnAddress = new byte[20]; return defaultHapiSpec( "GetTokenDefaultFreezeStatus", @@ -115,25 +116,47 @@ final HapiSpec getTokenDefaultFreezeStatus() { .payingWith(ACCOUNT) .via("GetTokenDefaultFreezeStatusTx") .gas(GAS_TO_OFFER), + // ,contractCall( + // TOKEN_DEFAULT_KYC_FREEZE_STATUS_CONTRACT, + // GET_TOKEN_DEFAULT_FREEZE, + // HapiParserUtil.asHeadlongAddress(notAnAddress)) + // .payingWith(ACCOUNT) + // .via("fakeAddressDefaultFreezeStatus") + // .hasKnownStatus(CONTRACT_REVERT_EXECUTED), contractCallLocal( TOKEN_DEFAULT_KYC_FREEZE_STATUS_CONTRACT, GET_TOKEN_DEFAULT_FREEZE, HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())))))) - .then(childRecordsCheck( - "GetTokenDefaultFreezeStatusTx", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.GET_TOKEN_DEFAULT_FREEZE_STATUS) - .withStatus(SUCCESS) - .withTokenDefaultFreezeStatus(true))))); + .then( + childRecordsCheck( + "GetTokenDefaultFreezeStatusTx", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.GET_TOKEN_DEFAULT_FREEZE_STATUS) + .withStatus(SUCCESS) + .withTokenDefaultFreezeStatus(true)))) + // ,childRecordsCheck( + // "fakeAddressDefaultFreezeStatus", + // CONTRACT_REVERT_EXECUTED, + // recordWith() + // .status(FAIL_INVALID) + // .contractCallResult(resultWith() + // .contractCallResult(htsPrecompileResult() + // + // .forFunction(FunctionType.GET_TOKEN_DEFAULT_FREEZE_STATUS) + // .withStatus(FAIL_INVALID) + // + // .withTokenDefaultFreezeStatus(false)))) + ); } @HapiTest final HapiSpec getTokenDefaultKycStatus() { final AtomicReference vanillaTokenID = new AtomicReference<>(); + final var notAnAddress = new byte[20]; return defaultHapiSpec( "GetTokenDefaultKycStatus", @@ -161,19 +184,40 @@ final HapiSpec getTokenDefaultKycStatus() { .payingWith(ACCOUNT) .via("GetTokenDefaultKycStatusTx") .gas(GAS_TO_OFFER), + // ,contractCall( + // TOKEN_DEFAULT_KYC_FREEZE_STATUS_CONTRACT, + // GET_TOKEN_DEFAULT_KYC, + // HapiParserUtil.asHeadlongAddress(notAnAddress)) + // .payingWith(ACCOUNT) + // .via("fakeAddressDefaultKycStatus") + // .hasKnownStatus(CONTRACT_REVERT_EXECUTED), contractCallLocal( TOKEN_DEFAULT_KYC_FREEZE_STATUS_CONTRACT, GET_TOKEN_DEFAULT_KYC, HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())))))) - .then(childRecordsCheck( - "GetTokenDefaultKycStatusTx", - SUCCESS, - recordWith() - .status(SUCCESS) - .contractCallResult(resultWith() - .contractCallResult(htsPrecompileResult() - .forFunction(FunctionType.GET_TOKEN_DEFAULT_KYC_STATUS) - .withStatus(SUCCESS) - .withTokenDefaultKycStatus(false))))); + .then( + childRecordsCheck( + "GetTokenDefaultKycStatusTx", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.GET_TOKEN_DEFAULT_KYC_STATUS) + .withStatus(SUCCESS) + .withTokenDefaultKycStatus(false)))) + // ,childRecordsCheck( + // "fakeAddressDefaultKycStatus", + // CONTRACT_REVERT_EXECUTED, + // recordWith() + // .status(FAIL_INVALID) + // .contractCallResult(resultWith() + // .contractCallResult(htsPrecompileResult() + // + // .forFunction(FunctionType.GET_TOKEN_DEFAULT_KYC_STATUS) + // .withStatus(FAIL_INVALID) + // + // .withTokenDefaultFreezeStatus(false)))) + ); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ERCPrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ERCPrecompileSuite.java index 38880b858de1..0113cf12c45f 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ERCPrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/ERCPrecompileSuite.java @@ -913,7 +913,7 @@ final HapiSpec getErc20TokenDecimalsFromErc721TokenFails() { @HapiTest final HapiSpec getErc721TokenName() { return defaultHapiSpec( - "getErc721TokenName", NONDETERMINISTIC_TRANSACTION_FEES, NONDETERMINISTIC_FUNCTION_PARAMETERS) + "getErc721TokenName", HIGHLY_NON_DETERMINISTIC_FEES, NONDETERMINISTIC_FUNCTION_PARAMETERS) .given( newKeyNamed(MULTI_KEY), cryptoCreate(ACCOUNT).balance(100 * ONE_HUNDRED_HBARS), diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/FreezeUnfreezeTokenPrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/FreezeUnfreezeTokenPrecompileSuite.java index ab1bf2ddb411..8e144581d337 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/FreezeUnfreezeTokenPrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/FreezeUnfreezeTokenPrecompileSuite.java @@ -146,6 +146,7 @@ final HapiSpec isFrozenHappyPathWithAliasLocalCall() { final AtomicReference vanillaTokenID = new AtomicReference<>(); final AtomicReference autoCreatedAccountId = new AtomicReference<>(); final String accountAlias = "accountAlias"; + final var notAnAddress = new byte[20]; return defaultHapiSpec("isFrozenHappyPathWithAliasLocalCall", NONDETERMINISTIC_FUNCTION_PARAMETERS) .given( @@ -163,13 +164,40 @@ final HapiSpec isFrozenHappyPathWithAliasLocalCall() { .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), uploadInitCode(FREEZE_CONTRACT), contractCreate(FREEZE_CONTRACT)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCallLocal( - FREEZE_CONTRACT, - IS_FROZEN_FUNC, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(autoCreatedAccountId.get()))))) - .then(); + .when( + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCallLocal( + FREEZE_CONTRACT, + IS_FROZEN_FUNC, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(autoCreatedAccountId.get())))) + // ,contractCall( + // FREEZE_CONTRACT, + // IS_FROZEN_FUNC, + // HapiParserUtil.asHeadlongAddress(notAnAddress), + // HapiParserUtil.asHeadlongAddress(notAnAddress)) + // .payingWith(GENESIS) + // .gas(GAS_TO_OFFER) + // .via("fakeAddressIsFrozen") + // .hasKnownStatus(CONTRACT_REVERT_EXECUTED) + + ) + .then( + // withOpContext(((spec, assertLog) -> allRunFor( + // spec, + // childRecordsCheck( + // "fakeAddressIsFrozen", + // CONTRACT_REVERT_EXECUTED, + // recordWith() + // .status(INVALID_TOKEN_ID) + // .contractCallResult(resultWith() + // + // .contractCallResult(htsPrecompileResult() + // + // .forFunction(FunctionType.HAPI_IS_FROZEN) + // .withStatus(INVALID_TOKEN_ID) + // .withIsFrozen(false))))))) + ); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/GrantRevokeKycSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/GrantRevokeKycSuite.java index 2c9306d35ee2..7bccaf51b493 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/GrantRevokeKycSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/GrantRevokeKycSuite.java @@ -277,6 +277,7 @@ final HapiSpec grantRevokeKycSpecWithAliasLocalCall() { final AtomicReference vanillaTokenID = new AtomicReference<>(); final AtomicReference autoCreatedAccountId = new AtomicReference<>(); final String accountAlias = "accountAlias"; + final var notAnAddress = new byte[20]; return defaultHapiSpec("grantRevokeKycSpecWithAliasLocalCall") .given( @@ -294,13 +295,40 @@ final HapiSpec grantRevokeKycSpecWithAliasLocalCall() { .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), uploadInitCode(GRANT_REVOKE_KYC_CONTRACT), contractCreate(GRANT_REVOKE_KYC_CONTRACT)) - .when(withOpContext((spec, opLog) -> allRunFor( - spec, - contractCallLocal( - GRANT_REVOKE_KYC_CONTRACT, - IS_KYC_GRANTED, - HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), - HapiParserUtil.asHeadlongAddress(autoCreatedAccountId.get()))))) - .then(); + .when( + withOpContext((spec, opLog) -> allRunFor( + spec, + contractCallLocal( + GRANT_REVOKE_KYC_CONTRACT, + IS_KYC_GRANTED, + HapiParserUtil.asHeadlongAddress(asAddress(vanillaTokenID.get())), + HapiParserUtil.asHeadlongAddress(autoCreatedAccountId.get())))) + // ,contractCall( + // GRANT_REVOKE_KYC_CONTRACT, + // IS_KYC_GRANTED, + // HapiParserUtil.asHeadlongAddress(notAnAddress), + // HapiParserUtil.asHeadlongAddress(notAnAddress)) + // .payingWith(GENESIS) + // .gas(GAS_TO_OFFER) + // .via("fakeAddressIsKyc") + // .hasKnownStatus(CONTRACT_REVERT_EXECUTED) + + ) + .then( + // withOpContext(((spec, assertLog) -> allRunFor( + // spec, + // childRecordsCheck( + // "fakeAddressIsKyc", + // CONTRACT_REVERT_EXECUTED, + // recordWith() + // .status(INVALID_TOKEN_ID) + // .contractCallResult(resultWith() + // + // .contractCallResult(htsPrecompileResult() + // + // .forFunction(FunctionType.HAPI_IS_KYC) + // .withStatus(INVALID_TOKEN_ID) + // .withIsFrozen(false))))))) + ); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenAndTypeCheckSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenAndTypeCheckSuite.java index 04935015cc34..56d7ccac0a51 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenAndTypeCheckSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenAndTypeCheckSuite.java @@ -16,6 +16,7 @@ package com.hedera.services.bdd.suites.contract.precompile; +import static com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType.HAPI_IS_TOKEN; import static com.hedera.services.bdd.junit.TestTags.SMART_CONTRACT; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; import static com.hedera.services.bdd.spec.assertions.ContractFnResultAsserts.isLiteralResult; @@ -40,7 +41,6 @@ import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; -import com.hedera.node.app.hapi.utils.contracts.ParsingConstants; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.HapiTestSuite; import com.hedera.services.bdd.spec.HapiSpec; @@ -169,12 +169,19 @@ final HapiSpec checkTokenAndTypeNegativeCases() { .status(SUCCESS) .contractCallResult(resultWith() .contractCallResult(htsPrecompileResult() - .forFunction(ParsingConstants.FunctionType.HAPI_IS_TOKEN) + .forFunction(HAPI_IS_TOKEN) .withStatus(SUCCESS) .withIsToken(false)))), childRecordsCheck( "FakeAddressTokenTypeCheckTx", CONTRACT_REVERT_EXECUTED, - recordWith().status(INVALID_TOKEN_ID))); + recordWith().status(INVALID_TOKEN_ID) + // .contractCallResult(resultWith() + // + // .contractCallResult(htsPrecompileResult() + // .forFunction(HAPI_IS_TOKEN) + // .withStatus(INVALID_TOKEN_ID) + // .withIsToken(false))) + )); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenExpiryInfoSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenExpiryInfoSuite.java index f64d810f9d9f..2f972aa524d5 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenExpiryInfoSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenExpiryInfoSuite.java @@ -137,7 +137,16 @@ final HapiSpec getExpiryInfoForToken() { childRecordsCheck( "expiryForInvalidTokenIDTxn", CONTRACT_REVERT_EXECUTED, - recordWith().status(INVALID_TOKEN_ID)), + recordWith().status(INVALID_TOKEN_ID) + // .contractCallResult(resultWith() + // + // .contractCallResult(htsPrecompileResult() + // + // .forFunction(FunctionType.HAPI_GET_TOKEN_EXPIRY_INFO) + // .withStatus(INVALID_TOKEN_ID) + // .withExpiry(0, + // AccountID.getDefaultInstance(), 0))) + ), withOpContext((spec, opLog) -> { final var getTokenInfoQuery = getTokenInfo(VANILLA_TOKEN); allRunFor(spec, getTokenInfoQuery); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenInfoHTSSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenInfoHTSSuite.java index 94479cc6242e..9d14e8b45448 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenInfoHTSSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenInfoHTSSuite.java @@ -50,6 +50,7 @@ import static com.hedera.services.bdd.spec.utilops.records.SnapshotMatchMode.NONDETERMINISTIC_FUNCTION_PARAMETERS; import static com.hedera.services.bdd.suites.contract.Utils.asAddress; import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; import com.google.protobuf.ByteString; @@ -560,17 +561,65 @@ final HapiSpec getInfoOnInvalidFungibleTokenFails() { HapiParserUtil.asHeadlongAddress(new byte[20])) .via(TOKEN_INFO_TXN + 1) .gas(1_000_000L) - .hasKnownStatus(ResponseCodeEnum.CONTRACT_REVERT_EXECUTED), + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), contractCall( TOKEN_INFO_CONTRACT, GET_INFORMATION_FOR_FUNGIBLE_TOKEN, HapiParserUtil.asHeadlongAddress(new byte[20])) .via(TOKEN_INFO_TXN + 2) .gas(1_000_000L) - .hasKnownStatus(ResponseCodeEnum.CONTRACT_REVERT_EXECUTED)))) - .then( + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)))) + .then(withOpContext((spec, opLog) -> allRunFor( + spec, getTxnRecord(TOKEN_INFO_TXN + 1).andAllChildRecords().logged(), - getTxnRecord(TOKEN_INFO_TXN + 2).andAllChildRecords().logged()); + getTxnRecord(TOKEN_INFO_TXN + 2).andAllChildRecords().logged() + + // ,childRecordsCheck( + // TOKEN_INFO_TXN + 2, + // CONTRACT_REVERT_EXECUTED, + // recordWith() + // .status(INVALID_TOKEN_ID) + // .contractCallResult(resultWith() + // .contractCallResult(htsPrecompileResult() + // + // .forFunction(FunctionType.HAPI_GET_FUNGIBLE_TOKEN_INFO) + // .withStatus(INVALID_TOKEN_ID) + // + // .withTokenInfo(getTokenInfoStructForEmptyFungibleToken( + // "", + // "", + // "", + // + // AccountID.getDefaultInstance(), + // 0, + // ByteString.EMPTY + // ))))) + // ,childRecordsCheck( + // TOKEN_INFO_TXN + 1, + // CONTRACT_REVERT_EXECUTED, + // recordWith() + // .status(INVALID_TOKEN_ID) + // + // .contractCallResult(resultWith() + // + // .contractCallResult(htsPrecompileResult() + // + // .forFunction(FunctionType.HAPI_GET_TOKEN_INFO) + // + // .withStatus(INVALID_TOKEN_ID) + // + // .withTokenInfo(getTokenInfoStructForEmptyFungibleToken( + // "", + // "", + // "", + // + // AccountID.getDefaultInstance(), + // 0, + // + // ByteString.EMPTY + // ))))) + + ))); } @HapiTest @@ -655,7 +704,7 @@ final HapiSpec getInfoOnInvalidNonFungibleTokenFails() { 1L) .via(NON_FUNGIBLE_TOKEN_INFO_TXN + 1) .gas(1_000_000L) - .hasKnownStatus(ResponseCodeEnum.CONTRACT_REVERT_EXECUTED), + .hasKnownStatus(CONTRACT_REVERT_EXECUTED), contractCall( TOKEN_INFO_CONTRACT, GET_INFORMATION_FOR_NON_FUNGIBLE_TOKEN, @@ -664,11 +713,37 @@ final HapiSpec getInfoOnInvalidNonFungibleTokenFails() { 2L) .via(NON_FUNGIBLE_TOKEN_INFO_TXN + 2) .gas(1_000_000L) - .hasKnownStatus(ResponseCodeEnum.CONTRACT_REVERT_EXECUTED)))) + .hasKnownStatus(CONTRACT_REVERT_EXECUTED)))) .then( getTxnRecord(NON_FUNGIBLE_TOKEN_INFO_TXN + 1) .andAllChildRecords() .logged(), + // ,childRecordsCheck( + // NON_FUNGIBLE_TOKEN_INFO_TXN + 1, + // CONTRACT_REVERT_EXECUTED, + // recordWith() + // .status(INVALID_TOKEN_ID) + // + // .contractCallResult(resultWith() + // + // .contractCallResult(htsPrecompileResult() + // + // .forFunction(FunctionType.HAPI_GET_NON_FUNGIBLE_TOKEN_INFO) + // + // .withStatus(INVALID_TOKEN_ID) + // + // .withTokenInfo(getTokenInfoStructForEmptyFungibleToken( + // "", + // "", + // "", + // + // AccountID.getDefaultInstance(), + // 0, + // + // ByteString.EMPTY + // )) + // + // .withNftTokenInfo(getEmptyNft())))), getTxnRecord(NON_FUNGIBLE_TOKEN_INFO_TXN + 2) .andAllChildRecords() .logged()); @@ -714,6 +789,13 @@ final HapiSpec happyPathGetTokenCustomFees() { asAddress(spec.registry().getTokenID(PRIMARY_TOKEN_NAME)))) .via(TOKEN_INFO_TXN) .gas(1_000_000L), + // , contractCall( + // TOKEN_INFO_CONTRACT, + // GET_CUSTOM_FEES_FOR_TOKEN, + // HapiParserUtil.asHeadlongAddress(new byte[20])) + // .via("fakeAddressTokenInfo") + // .gas(1_000_000L) + // .hasKnownStatus(CONTRACT_REVERT_EXECUTED), contractCallLocal( TOKEN_INFO_CONTRACT, GET_CUSTOM_FEES_FOR_TOKEN, @@ -731,7 +813,19 @@ final HapiSpec happyPathGetTokenCustomFees() { .contractCallResult(htsPrecompileResult() .forFunction(FunctionType.HAPI_GET_TOKEN_CUSTOM_FEES) .withStatus(SUCCESS) - .withCustomFees(getExpectedCustomFees(spec)))))))); + .withCustomFees(getExpectedCustomFees(spec))))) + // ,childRecordsCheck( + // "fakeAddressTokenInfo", + // CONTRACT_REVERT_EXECUTED, + // recordWith() + // .status(INVALID_TOKEN_ID) + // .contractCallResult(resultWith() + // .contractCallResult(htsPrecompileResult() + // + // .forFunction(FunctionType.HAPI_GET_TOKEN_CUSTOM_FEES) + // .withStatus(INVALID_TOKEN_ID) + // .withCustomFees(new ArrayList<>())))) + ))); } @HapiTest @@ -834,6 +928,17 @@ private TokenNftInfo getTokenNftInfoForCheck( .build(); } + private TokenNftInfo getEmptyNft() { + return TokenNftInfo.newBuilder() + .setLedgerId(ByteString.empty()) + .setNftID(NftID.getDefaultInstance()) + .setAccountID(AccountID.getDefaultInstance()) + .setCreationTime(Timestamp.newBuilder().build()) + .setMetadata(ByteString.empty()) + .setSpenderId(AccountID.getDefaultInstance()) + .build(); + } + private TokenInfo getTokenInfoStructForFungibleToken( final HapiSpec spec, final String tokenName, @@ -871,6 +976,39 @@ private TokenInfo getTokenInfoStructForFungibleToken( .build(); } + private TokenInfo getTokenInfoStructForEmptyFungibleToken( + final String tokenName, + final String symbol, + final String memo, + final AccountID treasury, + final long expirySecond, + ByteString ledgerId) { + + final ArrayList customFees = new ArrayList<>(); + + return TokenInfo.newBuilder() + .setLedgerId(ledgerId) + .setSupplyTypeValue(0) + .setExpiry(Timestamp.newBuilder().setSeconds(expirySecond)) + .setAutoRenewAccount(AccountID.getDefaultInstance()) + .setAutoRenewPeriod(Duration.newBuilder().setSeconds(0).build()) + .setSymbol(symbol) + .setName(tokenName) + .setMemo(memo) + .setTreasury(treasury) + .setTotalSupply(0) + .setMaxSupply(0) + .addAllCustomFees(customFees) + .setAdminKey(Key.newBuilder().build()) + .setKycKey(Key.newBuilder().build()) + .setFreezeKey(Key.newBuilder().build()) + .setWipeKey(Key.newBuilder().build()) + .setSupplyKey(Key.newBuilder().build()) + .setFeeScheduleKey(Key.newBuilder().build()) + .setPauseKey(Key.newBuilder().build()) + .build(); + } + @NonNull private ArrayList getExpectedCustomFees(final HapiSpec spec) { final var fixedFee = FixedFee.newBuilder().setAmount(500L).build(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenUpdatePrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenUpdatePrecompileSuite.java index cd1a7a3c51b9..6adf5b42e76c 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenUpdatePrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenUpdatePrecompileSuite.java @@ -18,6 +18,7 @@ import static com.hedera.services.bdd.junit.TestTags.SMART_CONTRACT; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; +import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.recordWith; import static com.hedera.services.bdd.spec.keys.KeyShape.CONTRACT; import static com.hedera.services.bdd.spec.keys.KeyShape.DELEGATE_CONTRACT; import static com.hedera.services.bdd.spec.keys.KeyShape.ED25519; @@ -40,6 +41,10 @@ import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; 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.HIGHLY_NON_DETERMINISTIC_FEES; +import static com.hedera.services.bdd.spec.utilops.records.SnapshotMatchMode.NONDETERMINISTIC_CONTRACT_CALL_RESULTS; +import static com.hedera.services.bdd.spec.utilops.records.SnapshotMatchMode.NONDETERMINISTIC_FUNCTION_PARAMETERS; +import static com.hedera.services.bdd.spec.utilops.records.SnapshotMatchMode.NONDETERMINISTIC_TRANSACTION_FEES; import static com.hedera.services.bdd.suites.contract.Utils.asAddress; import static com.hedera.services.bdd.suites.contract.Utils.asToken; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; @@ -52,7 +57,6 @@ 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.assertions.TransactionRecordAsserts; import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; import com.hedera.services.bdd.suites.HapiSuite; import com.hederahashgraph.api.proto.java.TokenID; @@ -64,7 +68,7 @@ import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.Tag; -@HapiTestSuite +@HapiTestSuite(fuzzyMatch = true) @Tag(SMART_CONTRACT) public class TokenUpdatePrecompileSuite extends HapiSuite { @@ -123,7 +127,10 @@ List negativeCases() { @HapiTest final HapiSpec updateTokenWithInvalidKeyValues() { final AtomicReference vanillaTokenID = new AtomicReference<>(); - return defaultHapiSpec("updateTokenWithInvalidKeyValues") + return defaultHapiSpec( + "updateTokenWithInvalidKeyValues", + NONDETERMINISTIC_TRANSACTION_FEES, + NONDETERMINISTIC_FUNCTION_PARAMETERS) .given( newKeyNamed(ED25519KEY).shape(ED25519), newKeyNamed(ECDSA_KEY).shape(SECP256K1), @@ -171,7 +178,11 @@ final HapiSpec updateTokenWithInvalidKeyValues() { @HapiTest public HapiSpec updateNftTokenKeysWithWrongTokenIdAndMissingAdminKey() { final AtomicReference nftToken = new AtomicReference<>(); - return defaultHapiSpec("updateNftTokenKeysWithWrongTokenIdAndMissingAdminKey") + return defaultHapiSpec( + "updateNftTokenKeysWithWrongTokenIdAndMissingAdminKey", + HIGHLY_NON_DETERMINISTIC_FEES, + NONDETERMINISTIC_FUNCTION_PARAMETERS, + NONDETERMINISTIC_CONTRACT_CALL_RESULTS) .given( cryptoCreate(TOKEN_TREASURY), newKeyNamed(ED25519KEY).shape(ED25519), @@ -240,17 +251,21 @@ public HapiSpec updateNftTokenKeysWithWrongTokenIdAndMissingAdminKey() { childRecordsCheck( UPDATE_TXN, CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith().status(INVALID_TOKEN_ID)), + recordWith().status(INVALID_TOKEN_ID)), childRecordsCheck( NO_ADMIN_KEY, CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith().status(TOKEN_IS_IMMUTABLE))))); + recordWith().status(TOKEN_IS_IMMUTABLE))))); } @HapiTest public HapiSpec getTokenKeyForNonFungibleNegative() { final AtomicReference nftToken = new AtomicReference<>(); - return defaultHapiSpec("getTokenKeyForNonFungibleNegative") + return defaultHapiSpec( + "getTokenKeyForNonFungibleNegative", + HIGHLY_NON_DETERMINISTIC_FEES, + NONDETERMINISTIC_FUNCTION_PARAMETERS, + NONDETERMINISTIC_CONTRACT_CALL_RESULTS) .given( cryptoCreate(TOKEN_TREASURY), newKeyNamed(MULTI_KEY).shape(ED25519_ON), @@ -298,10 +313,19 @@ public HapiSpec getTokenKeyForNonFungibleNegative() { childRecordsCheck( "InvalidTokenId", CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith().status(INVALID_TOKEN_ID)), + recordWith().status(INVALID_TOKEN_ID)), childRecordsCheck( NO_ADMIN_KEY, CONTRACT_REVERT_EXECUTED, - TransactionRecordAsserts.recordWith().status(KEY_NOT_PROVIDED))))); + recordWith().status(KEY_NOT_PROVIDED) + // .contractCallResult(ContractFnResultAsserts.resultWith() + // + // .contractCallResult(htsPrecompileResult() + // + // .forFunction(FunctionType.HAPI_GET_TOKEN_KEY) + // .withStatus(KEY_NOT_PROVIDED) + // + // .withTokenKeyValue(Key.newBuilder().build())))) + )))); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoTransferSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoTransferSuite.java index f3e1686ac4c0..cc6a7380c921 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoTransferSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoTransferSuite.java @@ -112,6 +112,7 @@ import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_AUTORENEW_ACCOUNT; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_CUSTOM_FEE_COLLECTOR; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SIGNATURE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.NO_REMAINING_AUTOMATIC_ASSOCIATIONS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SPENDER_DOES_NOT_HAVE_ALLOWANCE; @@ -241,7 +242,8 @@ public List getSpecsInSuite() { hapiTransferFromForFungibleTokenWithCustomFeesWithAllowance(), okToRepeatSerialNumbersInWipeList(), okToRepeatSerialNumbersInBurnList(), - canUseAliasAndAccountCombinations()); + canUseAliasAndAccountCombinations(), + transferInvalidTokenIdWithDecimals()); } @Override @@ -2114,6 +2116,23 @@ final HapiSpec hapiTransferFromForFungibleTokenWithCustomFeesWithAllowance() { .then(); } + @HapiTest + final HapiSpec transferInvalidTokenIdWithDecimals() { + return defaultHapiSpec("transferInvalidTokenIdWithDecimals", FULLY_NONDETERMINISTIC) + .given(cryptoCreate(TREASURY), withOpContext((spec, opLog) -> { + final var acctCreate = cryptoCreate(PAYER).balance(ONE_HUNDRED_HBARS); + allRunFor(spec, acctCreate); + // Here we take an account ID and store it as a token ID in the registry, so that when the "token + // number" is submitted by the test client, it will recreate the bug scenario: + final var bogusTokenId = TokenID.newBuilder().setTokenNum(acctCreate.numOfCreatedAccount()); + spec.registry().saveTokenId("nonexistent", bogusTokenId.build()); + })) + .when() + .then(sourcing(() -> cryptoTransfer( + movingWithDecimals(1L, "nonexistent", 2).betweenWithDecimals(PAYER, TREASURY)) + .hasKnownStatus(INVALID_TOKEN_ID))); + } + @Override protected Logger getResultsLogger() { return LOG; diff --git a/hedera-node/test-clients/src/main/resource/spec-default.properties b/hedera-node/test-clients/src/main/resource/spec-default.properties index 19469637b562..9b6f6d475841 100644 --- a/hedera-node/test-clients/src/main/resource/spec-default.properties +++ b/hedera-node/test-clients/src/main/resource/spec-default.properties @@ -38,6 +38,7 @@ default.nodePayment.tinyBars=5000 default.payer=0.0.2 recordStream.path=hedera-node/hedera-app/build/node/data/recordStreams/record0.0.3 recordStream.autoSnapshotManagement=false +recordStream.overrideExistingSnapshot=false #recordStream.autoSnapshotTarget=MONO_SERVICE recordStream.autoMatchTarget=HAPI_TEST recordStream.autoSnapshotTarget=MONO_SERVICE diff --git a/hedera-node/test-clients/src/main/resource/testfiles/config.txt b/hedera-node/test-clients/src/main/resource/testfiles/config.txt index 9b596e48e478..115c9a9b5b34 100644 --- a/hedera-node/test-clients/src/main/resource/testfiles/config.txt +++ b/hedera-node/test-clients/src/main/resource/testfiles/config.txt @@ -3,7 +3,6 @@ ############################################################################################### swirld, 123 -# app, HashgraphDemo.jar, 1,0,0,0,0,0,0,0,0,0, all # app, GameDemo.jar, 9000, 9000 # app, HelloSwirldDemo.jar # app, CryptocurrencyDemo.jar @@ -73,12 +72,6 @@ nextNodeId, 1 # # FilesystemDemo.jar parameters: none # -# HashGraphDemo.jar takes parameters that give the initial checkbox settings, -# in the same order they appear are on the screen, with 1 to check it -# and 0 to not check it, followed by the number of events to display -# (or “all”). The first parameter controls whether it runs -# slowly (1) or runs at full speed (0). -# # GameDemo.jar parameters: # height: height of the board (in cells). Player moves 1 cell at a time. # width: width of the board (in cells). Player moves 1 cell at a time. diff --git a/hedera-node/test-clients/src/main/resource/testfiles/updateFeature/updateConfigPortNumber/sdk/config.txt b/hedera-node/test-clients/src/main/resource/testfiles/updateFeature/updateConfigPortNumber/sdk/config.txt index f54750bfc290..356ee32f83dc 100755 --- a/hedera-node/test-clients/src/main/resource/testfiles/updateFeature/updateConfigPortNumber/sdk/config.txt +++ b/hedera-node/test-clients/src/main/resource/testfiles/updateFeature/updateConfigPortNumber/sdk/config.txt @@ -3,7 +3,6 @@ ############################################################################################### swirld, 123 -# app, HashgraphDemo.jar, 1,0,0,0,0,0,0,0,0,0, all # app, GameDemo.jar, 9000, 9000 # app, HelloSwirldDemo.jar # app, CryptocurrencyDemo.jar @@ -71,12 +70,6 @@ swirld, 123 # # FilesystemDemo.jar parameters: none # -# HashGraphDemo.jar takes parameters that give the initial checkbox settings, -# in the same order they appear are on the screen, with 1 to check it -# and 0 to not check it, followed by the number of events to display -# (or “all”). The first parameter controls whether it runs -# slowly (1) or runs at full speed (0). -# # GameDemo.jar parameters: # height: height of the board (in cells). Player moves 1 cell at a time. # width: width of the board (in cells). Player moves 1 cell at a time. diff --git a/hedera-node/test-clients/src/rcdiff/java/com/hedera/services/rcdiff/RcDiff.java b/hedera-node/test-clients/src/rcdiff/java/com/hedera/services/rcdiff/RcDiff.java index d4f0db8acb82..0bc3352ca674 100644 --- a/hedera-node/test-clients/src/rcdiff/java/com/hedera/services/rcdiff/RcDiff.java +++ b/hedera-node/test-clients/src/rcdiff/java/com/hedera/services/rcdiff/RcDiff.java @@ -16,6 +16,8 @@ package com.hedera.services.rcdiff; +import static com.hedera.node.app.hapi.utils.exports.recordstreaming.RecordStreamingUtils.orderedRecordFilesFrom; +import static com.hedera.node.app.hapi.utils.exports.recordstreaming.RecordStreamingUtils.parseRecordFileConsensusTime; import static com.hedera.node.app.hapi.utils.forensics.DifferingEntries.FirstEncounteredDifference.CONSENSUS_TIME_MISMATCH; import static com.hedera.node.app.hapi.utils.forensics.DifferingEntries.FirstEncounteredDifference.TRANSACTION_MISMATCH; import static com.hedera.node.app.hapi.utils.forensics.DifferingEntries.FirstEncounteredDifference.TRANSACTION_RECORD_MISMATCH; @@ -30,9 +32,13 @@ import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Paths; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.Callable; +import java.util.function.Predicate; import picocli.CommandLine; @Command(name = "rcdiff", description = "Diffs two record streams") @@ -46,6 +52,12 @@ public class RcDiff implements Callable { defaultValue = "10") Long maxDiffsToExport; + @Option( + names = {"-l", "--len-of-diff-secs"}, + paramLabel = "number of seconds to diff at a time", + defaultValue = "300") + Long lenOfDiffSecs; + @Option( names = {"-e", "--expected-stream"}, paramLabel = "location of expected stream files") @@ -78,7 +90,7 @@ public Integer call() throws Exception { } throw new AssertionError("No difference to summarize"); }; - final var diffs = findDifferencesBetweenV6(expectedStreamsLoc, actualStreamsLoc, recordDiffSummarizer, null); + final var diffs = diffsGiven(recordDiffSummarizer); if (diffs.isEmpty()) { System.out.println("These streams are identical ☺️"); return 0; @@ -89,6 +101,48 @@ public Integer call() throws Exception { } } + private List diffsGiven( + @NonNull final OrderedComparison.RecordDiffSummarizer recordDiffSummarizer) throws IOException { + final var actualBoundaries = boundaryTimesFor(actualStreamsLoc); + final var expectedBoundaries = boundaryTimesFor(expectedStreamsLoc); + final var first = Collections.min(List.of(actualBoundaries.first, expectedBoundaries.first)); + final var last = Collections.max(List.of(actualBoundaries.last, expectedBoundaries.last)); + final List diffs = new ArrayList<>(); + for (Instant i = first; !i.isAfter(last); i = i.plusSeconds(lenOfDiffSecs)) { + final var start = i; + final var end = i.plusSeconds(lenOfDiffSecs); + // Include files in the range [start, end) + final Predicate inclusionTest = f -> { + final var consensusTime = parseRecordFileConsensusTime(f); + return !consensusTime.isBefore(start) && consensusTime.isBefore(end); + }; + final var diffsHere = findDifferencesBetweenV6( + expectedStreamsLoc, + actualStreamsLoc, + recordDiffSummarizer, + inclusionTest, + "from " + start + ", before " + end); + diffs.addAll(diffsHere); + } + return diffs; + } + + record BoundaryTimes(Instant first, Instant last) {} + + private static BoundaryTimes boundaryTimesFor(@NonNull final String loc) { + try { + final var orderedFiles = orderedRecordFilesFrom(loc, f -> true); + if (orderedFiles.isEmpty()) { + return new BoundaryTimes(Instant.MAX, Instant.EPOCH); + } + return new BoundaryTimes( + parseRecordFileConsensusTime(orderedFiles.getFirst()), + parseRecordFileConsensusTime(orderedFiles.getLast())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + private void throwOnInvalidCommandLine() { if (actualStreamsLoc == null) { throw new ParameterException(spec.commandLine(), "Please specify an actual stream location"); @@ -96,6 +150,9 @@ private void throwOnInvalidCommandLine() { if (expectedStreamsLoc == null) { throw new ParameterException(spec.commandLine(), "Please specify an expected stream location"); } + if (lenOfDiffSecs <= 0) { + throw new ParameterException(spec.commandLine(), "Please specify a positive length of diff in seconds"); + } } private void dumpDiffs(@NonNull final List diffs) { diff --git a/hedera-node/test-clients/testfiles/config.txt b/hedera-node/test-clients/testfiles/config.txt index 586238c48b8f..a788f583b5eb 100644 --- a/hedera-node/test-clients/testfiles/config.txt +++ b/hedera-node/test-clients/testfiles/config.txt @@ -3,7 +3,6 @@ ############################################################################################### swirld, 123 -# app, HashgraphDemo.jar, 1,0,0,0,0,0,0,0,0,0, all # app, GameDemo.jar, 9000, 9000 # app, HelloSwirldDemo.jar # app, CryptocurrencyDemo.jar @@ -72,12 +71,6 @@ nextNodeId, 1 # # FilesystemDemo.jar parameters: none # -# HashGraphDemo.jar takes parameters that give the initial checkbox settings, -# in the same order they appear are on the screen, with 1 to check it -# and 0 to not check it, followed by the number of events to display -# (or “all”). The first parameter controls whether it runs -# slowly (1) or runs at full speed (0). -# # GameDemo.jar parameters: # height: height of the board (in cells). Player moves 1 cell at a time. # width: width of the board (in cells). Player moves 1 cell at a time. diff --git a/platform-sdk/platform-apps/demos/HashgraphDemo/build.gradle.kts b/platform-sdk/platform-apps/demos/HashgraphDemo/build.gradle.kts deleted file mode 100644 index 8fa39e51d1e7..000000000000 --- a/platform-sdk/platform-apps/demos/HashgraphDemo/build.gradle.kts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (C) 2020-2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -plugins { id("com.hedera.hashgraph.application") } - -application.mainClass.set("com.swirlds.demo.hashgraph.HashgraphDemoMain") diff --git a/platform-sdk/platform-apps/demos/HashgraphDemo/src/main/java/com/swirlds/demo/hashgraph/HashgraphDemoMain.java b/platform-sdk/platform-apps/demos/HashgraphDemo/src/main/java/com/swirlds/demo/hashgraph/HashgraphDemoMain.java deleted file mode 100644 index c2ea1f496fd1..000000000000 --- a/platform-sdk/platform-apps/demos/HashgraphDemo/src/main/java/com/swirlds/demo/hashgraph/HashgraphDemoMain.java +++ /dev/null @@ -1,506 +0,0 @@ -/* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.swirlds.demo.hashgraph; -/* - * This file is public domain. - * - * SWIRLDS 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. SWIRLDS SHALL NOT BE LIABLE FOR - * ANY DAMAGES SUFFERED AS A RESULT OF USING, MODIFYING OR - * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. - */ - -import static com.swirlds.metrics.api.Metrics.PLATFORM_CATEGORY; - -import com.swirlds.common.platform.NodeId; -import com.swirlds.metrics.api.Metrics; -import com.swirlds.platform.Browser; -import com.swirlds.platform.ParameterProvider; -import com.swirlds.platform.gui.GuiPlatformAccessor; -import com.swirlds.platform.gui.SwirldsGui; -import com.swirlds.platform.gui.model.GuiModel; -import com.swirlds.platform.network.Network; -import com.swirlds.platform.state.address.AddressBookNetworkUtils; -import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.Platform; -import com.swirlds.platform.system.SwirldMain; -import com.swirlds.platform.system.SwirldState; -import com.swirlds.platform.system.address.Address; -import com.swirlds.platform.system.address.AddressBook; -import com.swirlds.platform.system.events.PlatformEvent; -import java.awt.Checkbox; -import java.awt.Color; -import java.awt.Component; -import java.awt.Font; -import java.awt.FontMetrics; -import java.awt.Graphics; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.Label; -import java.awt.TextField; -import java.awt.geom.Rectangle2D; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.Arrays; -import java.util.Locale; -import java.util.function.BiFunction; -import javax.swing.JFrame; -import javax.swing.JPanel; - -/** - * This app draws the hashgraph on the screen. Events are circles, with earlier ones lower. Events are color - * coded: A non-witness is gray, and a witness has a color of green (famous), blue (not famous) or red - * (undecided fame). When the event becomes part of the consensus, its color becomes darker. - */ -public class HashgraphDemoMain implements SwirldMain { - /** delay after each screen update in milliseconds (250 means update 4 times per second) */ - private static final long screenUpdateDelay = 250; - - /** color for outline of labels */ - static final Color LABEL_OUTLINE = new Color(255, 255, 255); - /** color for unknown-fame witness, non-consensus */ - static final Color LIGHT_RED = new Color(192, 0, 0); - /** color for unknown-fame witness, consensus (which never happens) */ - static final Color DARK_RED = new Color(128, 0, 0); - /** color for famous witness, non-consensus */ - static final Color LIGHT_GREEN = new Color(0, 192, 0); - /** color for famous witness, consensus */ - static final Color DARK_GREEN = new Color(0, 128, 0); - /** color for non-famous witness, non-consensus */ - static final Color LIGHT_BLUE = new Color(0, 0, 192); - /** color for non-famous witness, consensus */ - static final Color DARK_BLUE = new Color(0, 0, 128); - /** color for non-witness, non-consensus */ - static final Color LIGHT_GRAY = new Color(160, 160, 160); - /** non-witness, consensus */ - static final Color DARK_GRAY = new Color(0, 0, 0); - /** the app is run by this */ - public Platform platform; - /** ID for this member */ - public NodeId selfId; - /** the entire window, including Swirlds menu, Picture, checkboxes */ - JFrame window; - /** the JFrame with the hashgraph */ - Picture picture; - /** a copy of the set of events at one moment, which paintComponent will draw */ - private PlatformEvent[] eventsCache; - /** the number of members in the addressBook */ - private int numMembers = -1; - /** number of columns (equals number of members) */ - private int numColumns; - /** the nicknames of all the members */ - private String[] names; - - /** if checked, this member calls to gossip once per second */ - private Checkbox slowCheckbox; - /** if checked, freeze the display (don't update it) */ - private Checkbox freezeCheckbox; - /** if checked, color vertices only green (non-consensus) or blue (consensus) */ - private Checkbox simpleColorsCheckbox; - - // the following checkboxes control which labels to print on each vertex - - /** the round number for the event */ - private Checkbox labelRoundCheckbox; - /** the consensus round received for the event */ - private Checkbox labelRoundRecCheckbox; - /** the consensus order number for the event */ - private Checkbox labelConsOrderCheckbox; - /** the consensus time stamp for the event */ - private Checkbox labelConsTimestampCheckbox; - /** the generation number for the event */ - private Checkbox labelGenerationCheckbox; - /** the ID number of the member who created the event */ - private Checkbox labelCreatorCheckbox; - - /** only draw this many events, at most */ - private TextField eventLimit; - - /** format the consensusTimestamp label */ - DateTimeFormatter formatter = - DateTimeFormatter.ofPattern("H:m:s.n").withLocale(Locale.US).withZone(ZoneId.systemDefault()); - - private static final BasicSoftwareVersion softwareVersion = new BasicSoftwareVersion(1); - - /** - * Return the color for an event based on calculations in the consensus algorithm A non-witness is gray, - * and a witness has a color of green (famous), blue (not famous) or red (undecided fame). When the - * event becomes part of the consensus, its color becomes darker. - * - * @param event - * the event to color - * @return its color - */ - private Color eventColor(final PlatformEvent event) { - if (simpleColorsCheckbox.getState()) { // if checkbox checked - return event.isConsensus() ? LIGHT_BLUE : LIGHT_GREEN; - } - if (!event.isWitness()) { - return event.isConsensus() ? DARK_GRAY : LIGHT_GRAY; - } - if (!event.isFameDecided()) { - return event.isConsensus() ? DARK_RED : LIGHT_RED; - } - if (event.isFamous()) { - return event.isConsensus() ? DARK_GREEN : LIGHT_GREEN; - } - return event.isConsensus() ? DARK_BLUE : LIGHT_BLUE; - } - - /** - * This panel has the statistics and hashgraph picture, and appears in the window below all the - * settings, right below "display last ___ events". - */ - private class Picture extends JPanel { - private static final long serialVersionUID = 1L; - int ymin, ymax, width, n; - double r; - long minGen, maxGen; - /** row to draw next in the window */ - int row; - /** column to draw next in the window */ - int col; - /** font height, in pixels */ - int textLineHeight; - - /** - * find x position on the screen for the given event event - * - * @param event - * the event (displayed as a circle on the screen) - * @return the x coordinate for that event - */ - private int xpos(final PlatformEvent event) { - // To support Noncontiguous NodeId, the index of the NodeId in the address book is used. - final int nodeIndex = platform.getAddressBook().getIndexOfNodeId(event.getCreatorId()); - return (nodeIndex + 1) * width / (numColumns + 1); - } - - /** - * find y position on the screen for the given event event - * - * @param event - * the event (displayed as a circle on the screen) - * @return the y coordinate for that event - */ - private int ypos(final PlatformEvent event) { - return (event == null) ? -100 : (int) (ymax - r * (1 + 2 * (event.getGeneration() - minGen))); - } - - /** - * called by paintComponent to draw text at the top of the window - * - * @param g - * the graphics context passed to paintComponent - * @param text - * a String.format formatting string - * @param value - * the value to pass to String.format to be formatted - */ - private void print(final Graphics g, final String text, final double value) { - g.drawString(String.format(text, value), col, row++ * textLineHeight - 3); - } - - /** {@inheritDoc} */ - public void paintComponent(final Graphics g) { - super.paintComponent(g); - g.setFont(new Font(Font.MONOSPACED, 12, 12)); - final FontMetrics fm = g.getFontMetrics(); - final int fa = fm.getMaxAscent(); - final int fd = fm.getMaxDescent(); - textLineHeight = fa + fd; - final int numMem = platform.getAddressBook().getSize(); - calcNames(); - width = getWidth(); - - row = 1; - col = 10; - final Metrics metrics = platform.getContext().getMetrics(); - final double createCons = (double) metrics.getValue(PLATFORM_CATEGORY, "secC2C"); - final double recCons = (double) metrics.getValue(PLATFORM_CATEGORY, "secR2C"); - - print(g, "%5.0f trans_per_sec", (double) metrics.getValue(PLATFORM_CATEGORY, "trans_per_sec")); - print(g, "%5.0f events_per_sec", (double) metrics.getValue(PLATFORM_CATEGORY, "events_per_sec")); - print(g, "%4.0f%% duplicate events", (double) metrics.getValue(PLATFORM_CATEGORY, "dupEvPercent")); - - print(g, "%5.3f sec, propagation time", createCons - recCons); - print(g, "%5.3f sec, create to consensus", createCons); - print(g, "%5.3f sec, receive to consensus", recCons); - final Address address = platform.getAddressBook().getAddress(platform.getSelfId()); - print(g, "Internal: " + Network.getInternalIPAddress() + " : " + address.getPortInternal(), 0); - - final int height1 = (row - 1) * textLineHeight; // text area at the top - final int height2 = getHeight() - height1; // the main display, below the text - g.setColor(Color.BLACK); - ymin = (int) Math.round(height1 + 0.025 * height2); - ymax = (int) Math.round(height1 + 0.975 * height2) - textLineHeight; - for (int i = 0; i < numColumns; i++) { - final int x = (i + 1) * width / (numColumns + 1); - g.drawLine(x, ymin, x, ymax); - final Rectangle2D rect = fm.getStringBounds(names[i], g); - g.drawString(names[i], (int) (x - rect.getWidth() / 2), (int) (ymax + rect.getHeight())); - } - - PlatformEvent[] events = eventsCache; - if (events == null) { // in case a screen refresh happens before any events - return; - } - int maxEvents; - try { - maxEvents = Math.max(0, Integer.parseInt(eventLimit.getText())); - } catch (final NumberFormatException err) { - maxEvents = 0; - } - - if (maxEvents > 0) { - events = Arrays.copyOfRange(events, Math.max(0, events.length - maxEvents), events.length); - } - - minGen = Integer.MAX_VALUE; - maxGen = Integer.MIN_VALUE; - for (final PlatformEvent event : events) { - minGen = Math.min(minGen, event.getGeneration()); - maxGen = Math.max(maxGen, event.getGeneration()); - } - maxGen = Math.max(maxGen, minGen + 2); - n = numMem + 1; - final double gens = maxGen - minGen; - final double dy = (ymax - ymin) * (gens - 1) / gens; - r = Math.min(width / n / 4, dy / gens / 2); - final int d = (int) (2 * r); - - // for each event, draw 2 downward lines to its parents - for (final PlatformEvent event : events) { - g.setColor(eventColor(event)); - final PlatformEvent e1 = event.getSelfParent(); - final PlatformEvent e2 = event.getOtherParent(); - if (e1 != null && e1.getGeneration() >= minGen) { - g.drawLine(xpos(event), ypos(event), xpos(event), ypos(e1)); - } - if (e2 != null && e2.getGeneration() >= minGen) { - g.drawLine(xpos(event), ypos(event), xpos(e2), ypos(e2)); - } - } - - // for each event, draw its circle - for (final PlatformEvent event : events) { - final PlatformEvent e1 = event.getSelfParent(); - final PlatformEvent e2 = event.getOtherParent(); - if (e1 == null || e2 == null) { - continue; // discarded events have no parents, so skip them - } - final Color color = eventColor(event); - g.setColor(color); - g.fillOval(xpos(event) - d / 2, ypos(event) - d / 2, d, d); - g.setFont(g.getFont().deriveFont(Font.BOLD)); - - String s = ""; - - if (labelRoundCheckbox.getState()) { - s += " " + event.getRoundCreated(); - } - if (labelRoundRecCheckbox.getState() && event.getRoundReceived() > 0) { - s += " " + event.getRoundReceived(); - } - // if not consensus, then there's no order yet - if (labelConsOrderCheckbox.getState() && event.isConsensus()) { - s += " " + event.getConsensusOrder(); - } - if (labelConsTimestampCheckbox.getState()) { - final Instant t = event.getConsensusTimestamp(); - if (t != null) { - s += " " + formatter.format(t); - } - } - if (labelGenerationCheckbox.getState()) { - s += " " + event.getGeneration(); - } - if (labelCreatorCheckbox.getState()) { - s += " " + event.getCreatorId(); // ID number of member who created it - } - if (s != "") { - final Rectangle2D rect = fm.getStringBounds(s, g); - final int x = (int) (xpos(event) - rect.getWidth() / 2. - fa / 4.); - final int y = (int) (ypos(event) + rect.getHeight() / 2. - fd / 2); - g.setColor(LABEL_OUTLINE); - g.drawString(s, x - 1, y - 1); - g.drawString(s, x + 1, y - 1); - g.drawString(s, x - 1, y + 1); - g.drawString(s, x + 1, y + 1); - g.setColor(color); - g.drawString(s, x, y); - } - } - } - } - - /** - * This is just for debugging: it allows the app to run in Eclipse. If the config.txt exists and lists a - * particular SwirldMain class as the one to run, then it can run in Eclipse (with the green triangle - * icon). - * - * @param args - * these are not used - */ - public static void main(final String[] args) { - Browser.parseCommandLineArgsAndLaunch(args); - } - - /** Fill in the names array, with the name of each member. Also set numColumns and numMembers. */ - private void calcNames() { - final AddressBook addressBook = platform.getAddressBook(); - numColumns = addressBook.getSize(); - if (numColumns != numMembers) { - numMembers = numColumns; - names = new String[numColumns]; - for (int i = 0; i < numColumns; i++) { - final NodeId nodeId = addressBook.getNodeId(i); - names[i] = addressBook.getAddress(nodeId).getNickname(); - } - } - } - - // //////////////////////////////////////////////////////////////////// - // the following are the methods required by the SwirldState interface - // //////////////////////////////////////////////////////////////////// - - @Override - public void init(final Platform platform, final NodeId id) { - this.platform = platform; - this.selfId = id; - final String[] parameters = ParameterProvider.getInstance().getParameters(); - - GuiModel.getInstance() - .setAbout( - platform.getSelfId(), - "Hashgraph Demo v. 1.1\n" + "\n" - + "trans_per_sec = # transactions added to the hashgraph per second\n" - + "events_per_sec = # events added to the hashgraph per second\n" - + "duplicate events = percentage of events a member receives that they already know.\n" - + "bad events_per_sec = number of events per second received by a member that are invalid.\n" - + "propagation time = average seconds from creating a new event to a given member receiving it.\n" - + "create to consensus = average seconds from creating a new event to knowing its consensus order.\n" - + "receive to consensus = average seconds from receiving an event to knowing its consensus order.\n" - + "Witnesses are colored circles, non-witnesses are black/gray.\n" - + "Dark circles are part of the consensus, light are not.\n" - + "Fame is true for green, false for blue, unknown for red.\n"); - final Address address = platform.getAddressBook().getAddress(platform.getSelfId()); - final int winCount = AddressBookNetworkUtils.getLocalAddressCount(platform.getAddressBook()); - final int winNum = GuiModel.getInstance().getInstanceNumber(platform.getSelfId()); - window = SwirldsGui.createWindow( - platform, address, winCount, winNum, false); // Uses BorderLayout. Size is chosen by the Platform - window.setLayout(new GridBagLayout()); // use a layout more powerful than BorderLayout - int p = 0; // which parameter to use - final BiFunction cb = (n, s) -> new Checkbox( - s, null, parameters.length > n && parameters[n].trim().equals("1")); - - slowCheckbox = cb.apply(p++, "Slow: this member initiates gossip once a second"); - freezeCheckbox = cb.apply(p++, "Freeze: don't change this window"); - simpleColorsCheckbox = cb.apply(p++, "Simple colors: blue for consensus, green for not"); - labelRoundCheckbox = cb.apply(p++, "Labels: Round created"); - labelRoundRecCheckbox = cb.apply(p++, "Labels: Round received (consensus)"); - labelConsOrderCheckbox = cb.apply(p++, "Labels: Order (consensus)"); - labelConsTimestampCheckbox = cb.apply(p++, "Labels: Timestamp (consensus)"); - labelGenerationCheckbox = cb.apply(p++, "Labels: Generation"); - labelCreatorCheckbox = cb.apply(p++, "Labels: Creator ID"); - - eventLimit = new TextField(parameters.length <= p ? "" : parameters[p].trim(), 5); - p++; - - final GridBagConstraints constr = new GridBagConstraints(); - constr.fill = GridBagConstraints.NONE; // don't stretch components - constr.gridwidth = GridBagConstraints.REMAINDER; // each component uses all cells in a row - constr.anchor = GridBagConstraints.WEST; // left align each component in its cell - constr.weightx = 0; // don't put extra space in the middle - constr.weighty = 0; - constr.gridx = 0; // start in upper-left cell - constr.gridy = 0; - constr.insets = new Insets(0, 10, -4, 0); // add external padding on left, remove from bottom - - final Component[] comps = new Component[] { - slowCheckbox, - freezeCheckbox, - simpleColorsCheckbox, - labelRoundCheckbox, - labelRoundRecCheckbox, - labelConsOrderCheckbox, - labelConsTimestampCheckbox, - labelGenerationCheckbox, - labelCreatorCheckbox - }; - for (final Component c : comps) { - window.add(c, constr); - constr.gridy++; - } - constr.gridwidth = 1; // each component is one cell - window.add(new Label("Display the last "), constr); - constr.gridx++; - constr.insets = new Insets(0, 0, -4, 0); // don't pad on left - window.add(eventLimit, constr); - constr.gridx++; - window.add(new Label(" events"), constr); - constr.gridx = 0; - constr.gridy++; // skip a line betwen settings and stats - window.add(new Label(" "), constr); - constr.gridy++; - constr.weighty = 1.0; // give the picture all leftover space - constr.weightx = 1.0; - constr.fill = GridBagConstraints.BOTH; // stretch the picture to fit - constr.gridwidth = GridBagConstraints.REMAINDER; // picture uses a whole row - picture = new Picture(); - window.add(picture, constr); - window.setVisible(true); - } - - @Override - public void run() { - while (true) { - if (window != null && !freezeCheckbox.getState()) { - eventsCache = GuiPlatformAccessor.getInstance().getAllEvents(platform.getSelfId()); - // after this getAllEvents call, the set of events to draw is frozen - // for the duration of this screen redraw. But their status (consensus or not) may change - // while it is being drawn. If an event is discarded while being drawn, then it forgets its - // parents, and won't be drawn here. - window.repaint(); - // the network will stop creating events if there are no user transactions. We will submit 1 byte - // transactions in order to have events created continuously - platform.createTransaction(new byte[1]); - } - try { - Thread.sleep(screenUpdateDelay); - } catch (final Exception e) { - } - } - } - - @Override - public SwirldState newState() { - return new HashgraphDemoState(); - } - - /** - * {@inheritDoc} - */ - @Override - public BasicSoftwareVersion getSoftwareVersion() { - return softwareVersion; - } -} diff --git a/platform-sdk/platform-apps/demos/HashgraphDemo/src/main/java/com/swirlds/demo/hashgraph/HashgraphDemoState.java b/platform-sdk/platform-apps/demos/HashgraphDemo/src/main/java/com/swirlds/demo/hashgraph/HashgraphDemoState.java deleted file mode 100644 index 0bbe6c55cdb4..000000000000 --- a/platform-sdk/platform-apps/demos/HashgraphDemo/src/main/java/com/swirlds/demo/hashgraph/HashgraphDemoState.java +++ /dev/null @@ -1,116 +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.demo.hashgraph; -/* - * This file is public domain. - * - * SWIRLDS 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. SWIRLDS SHALL NOT BE LIABLE FOR - * ANY DAMAGES SUFFERED AS A RESULT OF USING, MODIFYING OR - * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. - */ - -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.state.PlatformState; -import com.swirlds.platform.system.Round; -import com.swirlds.platform.system.SwirldState; - -/** - * The state for the hashgraph demo. See the comments for com.swirlds.demos.HashgraphDemoMain - */ -public class HashgraphDemoState extends PartialMerkleLeaf implements SwirldState, MerkleLeaf { - - /** - * The version history of this class. - * Versions that have been released must NEVER be given a different value. - */ - private static class ClassVersion { - /** - * In this version, serialization was performed by copyTo/copyToExtra and deserialization was performed by - * copyFrom/copyFromExtra. This version is not supported by later deserialization methods and must be handled - * specially by the platform. - */ - public static final int ORIGINAL = 1; - /** - * In this version, serialization was performed by serialize/deserialize. - */ - public static final int MIGRATE_TO_SERIALIZABLE = 2; - } - - private static final long CLASS_ID = 0xe8cc5e77ddbe70e4L; - - // /////////////////////////////////////////////////////////////////// - - public HashgraphDemoState() {} - - private HashgraphDemoState(final HashgraphDemoState sourceState) { - super(sourceState); - } - - @Override - public synchronized void handleConsensusRound(final Round round, final PlatformState platformState) {} - - /** - * {@inheritDoc} - */ - @Override - public synchronized HashgraphDemoState copy() { - throwIfImmutable(); - return new HashgraphDemoState(this); - } - - /** - * {@inheritDoc} - */ - @Override - public void serialize(final SerializableDataOutputStream out) {} - - /** - * {@inheritDoc} - */ - @Override - public void deserialize(final SerializableDataInputStream in, final int version) {} - - /** - * {@inheritDoc} - */ - @Override - public long getClassId() { - return CLASS_ID; - } - - /** - * {@inheritDoc} - */ - @Override - public int getVersion() { - return ClassVersion.MIGRATE_TO_SERIALIZABLE; - } - - /** - * {@inheritDoc} - */ - @Override - public int getMinimumSupportedVersion() { - return ClassVersion.MIGRATE_TO_SERIALIZABLE; - } -} diff --git a/platform-sdk/platform-apps/demos/HashgraphDemo/src/main/java/module-info.java b/platform-sdk/platform-apps/demos/HashgraphDemo/src/main/java/module-info.java deleted file mode 100644 index 838c2212dc87..000000000000 --- a/platform-sdk/platform-apps/demos/HashgraphDemo/src/main/java/module-info.java +++ /dev/null @@ -1,6 +0,0 @@ -module com.swirlds.demo.hashgraph { - requires com.swirlds.common; - requires com.swirlds.metrics.api; - requires com.swirlds.platform.core; - requires java.desktop; -} diff --git a/platform-sdk/platform-apps/tests/AddressBookTestingTool/README.md b/platform-sdk/platform-apps/tests/AddressBookTestingTool/README.md index b79276dc6957..61cd869e44a8 100644 --- a/platform-sdk/platform-apps/tests/AddressBookTestingTool/README.md +++ b/platform-sdk/platform-apps/tests/AddressBookTestingTool/README.md @@ -9,7 +9,7 @@ Be sure to clean and assemble the whole project if the java files have been modi ### config.txt comment out the current app ``` -# app, HashgraphDemo.jar, 1,0,0,0,0,0,0,0,0,0, all +# app, StatsDemo.jar, 1, 3000, 0, 100, -1, 200 ``` uncomment the AddressBookTestingTool.jar ``` diff --git a/platform-sdk/sdk/config.txt b/platform-sdk/sdk/config.txt index fd344d3b4180..45efeda015ad 100644 --- a/platform-sdk/sdk/config.txt +++ b/platform-sdk/sdk/config.txt @@ -3,10 +3,9 @@ ############################################################################################### swirld, 123 - app, HashgraphDemo.jar, 1,0,0,0,0,0,0,0,0,0, all # app, HelloSwirldDemo.jar # app, CryptocurrencyDemo.jar -# app, StatsDemo.jar, 1, 3000, 0, 100, -1, 200 + app, StatsDemo.jar, 1, 3000, 0, 100, -1, 200 # ** BEGIN REMOVE FROM SDK RELEASES ** @@ -95,12 +94,6 @@ nextNodeId, 4 # # FilesystemDemo.jar parameters: none # -# HashGraphDemo.jar takes parameters that give the initial checkbox settings, -# in the same order they appear are on the screen, with 1 to check it -# and 0 to not check it, followed by the number of events to display -# (or “all”). The first parameter controls whether it runs -# slowly (1) or runs at full speed (0). -# # GameDemo.jar parameters: # height: height of the board (in cells). Player moves 1 cell at a time. # width: width of the board (in cells). Player moves 1 cell at a time. diff --git a/platform-sdk/sdk/docs/index.html b/platform-sdk/sdk/docs/index.html index 1bfac215d3ae..934d96505d65 100644 --- a/platform-sdk/sdk/docs/index.html +++ b/platform-sdk/sdk/docs/index.html @@ -51,9 +51,6 @@

Useful documentation

The Swirlds SDK demo apps

- How to install / recompile the example apps in Eclipse -

- How to install / recompile the example apps from the command line

diff --git a/platform-sdk/sdk/docs/installCLI.html b/platform-sdk/sdk/docs/installCLI.html index 355dc5f580d8..cb85d9534b33 100755 --- a/platform-sdk/sdk/docs/installCLI.html +++ b/platform-sdk/sdk/docs/installCLI.html @@ -86,8 +86,8 @@

How to install / recompile the example apps from the command line

nano config.txt -
  • The file has a line near the top starting with HashgraphDemo, and another line near it starting with # - HelloSwirldDemo. Add a # character at the start of the HashgraphDemo line to comment it out, and +
  • The file has a line near the top starting with StatsDemo, and another line near it starting with # + HelloSwirldDemo. Add a # character at the start of the StatsDemo line to comment it out, and delete the # from the start of the HelloSwirldDemo line to uncomment it.
  • @@ -123,8 +123,7 @@

    How to install / recompile the example apps from the command line

    that output, it needs to be run from a console rather than by double clicking on the swirlds.jar file icon.
  • The above steps can be repeated to develop a new app, entirely from the command line. Though it may be easier to - use an Integrated Development Environment (IDE), such as developing the app in - Eclipse. + use an Integrated Development Environment (IDE).
  • diff --git a/platform-sdk/sdk/docs/installEclipse.html b/platform-sdk/sdk/docs/installEclipse.html deleted file mode 100755 index 8204044a563f..000000000000 --- a/platform-sdk/sdk/docs/installEclipse.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - -

    How to install / recompile the example apps in Eclipse

    - -These versions (or later) should be downloaded and installed: - - - -

    The above link is to OpenJDK 12, which is free. Oracle JDK 11+ requires a paid license.

    - -

    If the Swirlds browser freezes when it is first launched, it is possible that the operating system needs a utility - installed to collect entropy, such as the utilities for - Debian - or - Raspbian, which may also work on other versions of Linux or Unix. - -

    The examples below use slashes in paths, but on Windows they will be backslashes.

    - -Eclipse creates a workspace directory on your drive. Find it and ensure the files from the SDK are there. The sdk -directory should be put somewhere in the directory tree under the Eclipse workspace directory. For example, the -needed files might be put here: -
      -
    • workspace/git/sdk/data/apps/
    • -
    • workspace/git/sdk/swirlds.jar
    • -
    • workspace/git/sdk/config.txt
    • -
    - -

    The last one, config.txt, is used to make the Swirlds browser automatically run a particular app. It can even - make multiple instances of it run and communicate with each other, as if they were on multiple machines. The - comments in the file explain how to change it.

    - -

    The demo apps are now ready to run.

    - -

    The apps come with source code, and can be recompiled in Eclipse. To import the projects into Eclipse and recompile - them:

    - -
      - -
    • - import the demo projects -
        -
      • choose menu WINDOW > SHOW VIEW > OTHER > JAVA > PACKAGE EXPLORER (if the package explorer isn't already - visible) -
      • -
      • select menu FILE > IMPORT
      • -
      • click the triangle by MAVEN if it doesn't have entries under it
      • -
      • click EXISTING MAVEN PROJECTS
      • -
      • click NEXT
      • -
      • click the BROWSE button in the upper-right, on the same line as ROOT DIRECTORY
      • -
      • go to and select the sdk directory, such as workspace/git/sdk/
      • -
      • click OPEN
      • -
      • make sure the checkboxes by all the listed projects are checked
      • -
      • click FINISH
      • -
      -
    • - -
    • - recompile and run HashgraphDemo -
        -
      • edit the config.txt file to choose which app to run. The instructions are given as comments in - the file. -
      • -
      • in the package explorer, click on the triangle by HashgraphDemo to open it -
      • right click on the pom.xml under HashgraphDemo and choose RUN AS > MAVEN INSTALL
      • -
      • click on src under HashgraphDemo to select it
      • -
      • click the green arrow button on the top bar to run it
      • -
      • if it asks, click JAVA APPLICATION and click OK
      • -
      • it won't run yet, but that's ok
      • -
      • select menu RUN > RUN CONFIGURATIONS
      • -
      • select the ARGUMENTS tab
      • -
      • under WORKING DIRECTORY select OTHER
      • -
      • click FILESYSTEM and navigate to just inside the sdk directory
      • -
      • click OPEN
      • -
      • click APPLY
      • -
      • click CLOSE
      • -
      • in the package explorer click on src under HashgraphDemo to select it
      • -
      • click the green arrow button on the top bar to run it
      • -
      • the app should run. When you are done with it, choose QUIT under its menu, or close its window.
      • -
      -
    • -
    • recompile and run each of the other demos, using the above steps -
    - -

    At this point, all of the demos have been recompiled, and their new .jar files have been stored in the data/apps - folder for the browser to use. If you change the source code of one of the apps, repeat the above steps to recompile - it (do a MAVEN INSTALL) and run it (click the green arrow). It is possible to - run the new app, either in the browser, or from the command line, or in Eclipse - itself.

    - -

    - Back -

    - \ No newline at end of file diff --git a/platform-sdk/sdk/docs/runBrowser.html b/platform-sdk/sdk/docs/runBrowser.html index 2ae809061926..2c7a7b7ce15c 100644 --- a/platform-sdk/sdk/docs/runBrowser.html +++ b/platform-sdk/sdk/docs/runBrowser.html @@ -35,7 +35,6 @@

    Install Java and Swirlds

    The Swirlds browser needs both the Java runtime and the Swirlds SDK to be installed. This can be done by following the instructions for either -installing with Eclipse or installing with Maven. To simply run the apps without creating new apps, there is no need to install Eclipse or Maven. It is sufficient to just follow the installation instructions for the Java SDK and the JCE security policy file. diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/stream/EventStreamManager.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/stream/EventStreamManager.java index e810b3c1599c..4637c30bb5b3 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/stream/EventStreamManager.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/stream/EventStreamManager.java @@ -18,6 +18,7 @@ import static com.swirlds.base.units.UnitConstants.SECONDS_TO_MILLISECONDS; import static com.swirlds.logging.legacy.LogMarker.EVENT_STREAM; +import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; import static com.swirlds.metrics.api.Metrics.INFO_CATEGORY; import com.swirlds.base.time.Time; @@ -47,7 +48,6 @@ * runningHash for consensus Events. */ public class EventStreamManager { - /** use this for all logging, as controlled by the optional data/log4j2.xml file */ private static final Logger logger = LogManager.getLogger(EventStreamManager.class); /** @@ -59,18 +59,21 @@ public class EventStreamManager isLastEventInFreezeCheck; - /** receives consensus events from multiStream, then passes to hashCalculator */ + /** + * receives consensus events from multiStream, then passes to hashCalculator + */ private QueueThreadObjectStream hashQueueThread; /** - * receives consensus events from hashQueueThread, calculates this event's Hash, then passes to - * runningHashCalculator + * receives consensus events from multiStream, then passes to streamFileWriter */ - private HashCalculatorForStream hashCalculator; - /** receives consensus events from multiStream, then passes to streamFileWriter */ private QueueThreadObjectStream writeQueueThread; - /** receives consensus events from writeQueueThread, serializes consensus events to event stream files */ + /** + * receives consensus events from writeQueueThread, serializes consensus events to event stream files + */ private TimestampStreamFileWriter streamFileWriter; - /** initialHash loaded from signed state */ + /** + * initialHash loaded from signed state + */ private Hash initialHash = new ImmutableHash(new byte[DigestType.SHA_384.digestLength()]); /** * When we freeze the platform, the last event to be written to EventStream file is the last event in the freeze @@ -121,9 +124,9 @@ public EventStreamManager( eventStreamDir, eventsLogPeriod * SECONDS_TO_MILLISECONDS, signer, - /** when event streaming is started after reconnect, or at state recovering, - * startWriteAtCompleteWindow should be set to be true; when event streaming is started after - * restart, it should be set to be false */ + // when event streaming is started after reconnect, or at state recovering, + // startWriteAtCompleteWindow should be set to be true; when event streaming is started after + // restart, it should be set to be false false, EventStreamType.getInstance()); @@ -153,7 +156,10 @@ public EventStreamManager( // receives consensus events from hashCalculator, calculates and set runningHash for this event final RunningHashCalculatorForStream runningHashCalculator = new RunningHashCalculatorForStream<>(); - hashCalculator = new HashCalculatorForStream<>(runningHashCalculator); + + // receives consensus events from hashQueueThread, calculates this event's Hash, then passes to + // runningHashCalculator + final HashCalculatorForStream hashCalculator = new HashCalculatorForStream<>(runningHashCalculator); hashQueueThread = new QueueThreadObjectStreamConfiguration(threadManager) .setNodeId(selfId) .setComponent("event-stream") @@ -197,42 +203,61 @@ public void stop() { multiStream.close(); } - public void addEvents(final List events) { - events.forEach(this::addEvent); - } - /** - * receives a consensus event from ConsensusRoundHandler each time, sends it to multiStream which then sends to two - * queueThread for calculating runningHash and writing to file + * Adds a list of events to the event stream. * - * @param event the consensus event to be added + * @param events the list of events to add */ - public void addEvent(final T event) { - if (!freezePeriodStarted) { - multiStream.addObject(event); - if (isLastEventInFreezeCheck.test(event)) { - freezePeriodStarted = true; - logger.info( - EVENT_STREAM.getMarker(), - "ConsensusTimestamp of the last Event to be written into file before restarting: " + "{}", - event::getTimestamp); - multiStream.close(); + public void addEvents(@NonNull final List events) { + events.forEach(event -> { + if (!freezePeriodStarted) { + multiStream.addObject(event); + if (isLastEventInFreezeCheck.test(event)) { + freezePeriodStarted = true; + logger.info( + EVENT_STREAM.getMarker(), + "ConsensusTimestamp of the last Event to be written into file before restarting: {}", + event::getTimestamp); + multiStream.close(); + } + } else { + eventAfterFreezeLogger.warn( + EVENT_STREAM.getMarker(), "Event {} dropped after freezePeriodStarted!", event.getTimestamp()); } - } else { - eventAfterFreezeLogger.warn( - EVENT_STREAM.getMarker(), "Event {} dropped after freezePeriodStarted!", event.getTimestamp()); - } + }); } /** - * sets startWriteAtCompleteWindow: it should be set to be true after reconnect, or at state recovering; it should - * be set to be false at restart + * Updates the running hash with the given event hash. Called when a state is loaded. * - * @param startWriteAtCompleteWindow whether the writer should not write until the first complete window + * @param runningEventHashUpdate the hash to update the running hash with */ - public void setStartWriteAtCompleteWindow(final boolean startWriteAtCompleteWindow) { + public void updateRunningHash(@NonNull final RunningEventHashUpdate runningEventHashUpdate) { + try { + if (hashQueueThread != null) { + hashQueueThread.pause(); + } + if (writeQueueThread != null) { + writeQueueThread.pause(); + } + } catch (final InterruptedException e) { + logger.error(EXCEPTION.getMarker(), "Failed to pause queue threads", e); + Thread.currentThread().interrupt(); + } + if (streamFileWriter != null) { - streamFileWriter.setStartWriteAtCompleteWindow(startWriteAtCompleteWindow); + streamFileWriter.setStartWriteAtCompleteWindow(runningEventHashUpdate.isReconnect()); + } + + initialHash = new Hash(runningEventHashUpdate.runningEventHash()); + logger.info(EVENT_STREAM.getMarker(), "EventStreamManager::updateRunningHash: {}", initialHash); + multiStream.setRunningHash(initialHash); + + if (hashQueueThread != null) { + hashQueueThread.resume(); + } + if (writeQueueThread != null) { + writeQueueThread.resume(); } } @@ -241,11 +266,8 @@ public void setStartWriteAtCompleteWindow(final boolean startWriteAtCompleteWind * * @return current size of working queue for calculating hash and runningHash */ - public int getHashQueueSize() { - if (hashQueueThread == null) { - return 0; - } - return hashQueueThread.getQueue().size(); + private int getHashQueueSize() { + return hashQueueThread == null ? 0 : hashQueueThread.getQueue().size(); } /** @@ -253,63 +275,7 @@ public int getHashQueueSize() { * * @return current size of working queue for writing to event stream files */ - public int getEventStreamingQueueSize() { + private int getEventStreamingQueueSize() { return writeQueueThread == null ? 0 : writeQueueThread.getQueue().size(); } - - /** - * for unit testing - * - * @return current multiStream instance - */ - public MultiStream getMultiStream() { - return multiStream; - } - - /** - * for unit testing - * - * @return current TimestampStreamFileWriter instance - */ - public TimestampStreamFileWriter getStreamFileWriter() { - return streamFileWriter; - } - - /** - * for unit testing - * - * @return current HashCalculatorForStream instance - */ - public HashCalculatorForStream getHashCalculator() { - return hashCalculator; - } - - /** - * for unit testing - * - * @return whether freeze period has started - */ - public boolean getFreezePeriodStarted() { - return freezePeriodStarted; - } - - /** - * for unit testing - * - * @return a copy of initialHash - */ - public Hash getInitialHash() { - return new Hash(initialHash); - } - - /** - * sets initialHash after loading from signed state - * - * @param initialHash current runningHash of all consensus events - */ - public void setInitialHash(final Hash initialHash) { - this.initialHash = initialHash; - logger.info(EVENT_STREAM.getMarker(), "EventStreamManager::setInitialHash: {}", () -> initialHash); - multiStream.setRunningHash(initialHash); - } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/flow/StateHashedTrigger.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/stream/RunningEventHashUpdate.java similarity index 53% rename from platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/flow/StateHashedTrigger.java rename to platform-sdk/swirlds-common/src/main/java/com/swirlds/common/stream/RunningEventHashUpdate.java index 71c362c685f3..1620192f3587 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/flow/StateHashedTrigger.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/stream/RunningEventHashUpdate.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-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. @@ -14,25 +14,15 @@ * limitations under the License. */ -package com.swirlds.platform.dispatch.triggers.flow; +package com.swirlds.common.stream; import com.swirlds.common.crypto.Hash; -import com.swirlds.platform.dispatch.types.TriggerTwo; +import edu.umd.cs.findbugs.annotations.NonNull; /** - * Sends dispatches when a state is hashed. + * A record used to update the running event hash on various components when a new state is loaded + * + * @param runningEventHash the running event hash of the loaded state + * @param isReconnect whether or not this is a reconnect state */ -@FunctionalInterface -public interface StateHashedTrigger extends TriggerTwo { - - /** - * Signal that a state has been fully hashed. - * - * @param round - * the round of the state - * @param hash - * the hash of the state - */ - @Override - void dispatch(Long round, Hash hash); -} +public record RunningEventHashUpdate(@NonNull Hash runningEventHash, boolean isReconnect) {} diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/wires/output/OutputWire.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/wires/output/OutputWire.java index 4d8627370a6b..0086ebf7e021 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/wires/output/OutputWire.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/wiring/wires/output/OutputWire.java @@ -25,6 +25,7 @@ import com.swirlds.common.wiring.wires.SolderType; import com.swirlds.common.wiring.wires.input.InputWire; import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; @@ -90,6 +91,25 @@ public void solderTo(@NonNull final InputWire inputWire) { solderTo(inputWire, SolderType.PUT); } + /** + * A convenience method that should be used iff the order in which the {@code inputWires} are soldered is important. + * Using this method reduces the chance of inadvertent reordering when code is modified or reorganized. All + * invocations of this method should carefully document why the provided ordering is important. + *

    + * Since this method is specifically for input wires that require a certain order, at least two input wires must be + * provided. + * + * @param inputWires – an ordered list of the input wire to forward output data to + * @throws IllegalArgumentException if the size of {@code inputWires} is less than 2 + * @see #solderTo(InputWire) + */ + public void orderedSolderTo(@NonNull final List> inputWires) { + if (inputWires.size() < 2) { + throw new IllegalArgumentException("List must contain at least 2 input wires."); + } + inputWires.forEach(this::solderTo); + } + /** * Specify an input wire where output data should be passed. This forwarding operation respects back pressure. * diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/stream/EventStreamManagerTest.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/stream/EventStreamManagerTest.java index fe608d920448..305ebaaf6172 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/stream/EventStreamManagerTest.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/stream/EventStreamManagerTest.java @@ -16,150 +16,63 @@ package com.swirlds.common.stream; -import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; -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; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import com.swirlds.base.time.Time; import com.swirlds.common.crypto.Hash; -import com.swirlds.common.platform.NodeId; import com.swirlds.common.test.fixtures.RandomUtils; -import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; import com.swirlds.common.test.fixtures.stream.ObjectForTestStream; -import org.junit.jupiter.api.BeforeAll; +import java.util.List; +import java.util.Random; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; class EventStreamManagerTest { - private static final NodeId selfId = new NodeId(0); - private static final String nodeName = "node0"; - private static final String eventsLogDir = "eventStream/"; - private static final long eventsLogPeriod = 5; - private static final int eventStreamQueueCapacity = 100; - - private static final String INITIALIZE_NOT_NULL = "after initialization, the instance should not be null"; - private static final String INITIALIZE_QUEUE_EMPTY = "after initialization, hash queue should be empty"; - private static final String UNEXPECTED_VALUE = "unexpected value"; - - private static EventStreamManager disableStreamingInstance; - private static EventStreamManager enableStreamingInstance; - - private static final Hash initialHash = RandomUtils.randomHash(); - private static final MultiStream multiStreamMock = mock(MultiStream.class); - private static final EventStreamManager EVENT_STREAM_MANAGER = + private static final EventStreamManager eventStreamManager = new EventStreamManager<>(Time.getCurrent(), multiStreamMock, EventStreamManagerTest::isFreezeEvent); private static final ObjectForTestStream freezeEvent = mock(ObjectForTestStream.class); - @BeforeAll - static void init() throws Exception { - disableStreamingInstance = new EventStreamManager<>( - TestPlatformContextBuilder.create().build(), - Time.getCurrent(), - getStaticThreadManager(), - selfId, - mock(Signer.class), - nodeName, - false, - eventsLogDir, - eventsLogPeriod, - eventStreamQueueCapacity, - EventStreamManagerTest::isFreezeEvent); - - enableStreamingInstance = new EventStreamManager<>( - TestPlatformContextBuilder.create().build(), - Time.getCurrent(), - getStaticThreadManager(), - selfId, - mock(Signer.class), - nodeName, - true, - eventsLogDir, - eventsLogPeriod, - eventStreamQueueCapacity, - EventStreamManagerTest::isFreezeEvent); - } - - @Test - void initializeTest() { - assertNull( - disableStreamingInstance.getStreamFileWriter(), - "When eventStreaming is disabled, streamFileWriter instance should be null"); - assertNotNull(disableStreamingInstance.getMultiStream(), INITIALIZE_NOT_NULL); - assertNotNull(disableStreamingInstance.getHashCalculator(), INITIALIZE_NOT_NULL); - assertEquals(0, disableStreamingInstance.getHashQueueSize(), INITIALIZE_QUEUE_EMPTY); - assertEquals(0, disableStreamingInstance.getEventStreamingQueueSize(), INITIALIZE_QUEUE_EMPTY); - - assertNotNull( - enableStreamingInstance.getStreamFileWriter(), - "When eventStreaming is enabled, streamFileWriter instance should not be null"); - assertNotNull(enableStreamingInstance.getMultiStream(), INITIALIZE_NOT_NULL); - assertNotNull(enableStreamingInstance.getHashCalculator(), INITIALIZE_NOT_NULL); - assertEquals(0, enableStreamingInstance.getHashQueueSize(), INITIALIZE_QUEUE_EMPTY); - assertEquals(0, enableStreamingInstance.getEventStreamingQueueSize(), INITIALIZE_QUEUE_EMPTY); - } - - @Test - void setInitialHashTest() { - EVENT_STREAM_MANAGER.setInitialHash(initialHash); - verify(multiStreamMock).setRunningHash(initialHash); - assertEquals(initialHash, EVENT_STREAM_MANAGER.getInitialHash(), "initialHash is not set"); - } - @Test - void addEventTest() throws InterruptedException { - EventStreamManager eventStreamManager = - new EventStreamManager<>(Time.getCurrent(), multiStreamMock, EventStreamManagerTest::isFreezeEvent); - assertFalse( - eventStreamManager.getFreezePeriodStarted(), - "freezePeriodStarted should be false after initialization"); + void addEventTest() { final int nonFreezeEventsNum = 10; for (int i = 0; i < nonFreezeEventsNum; i++) { - ObjectForTestStream event = mock(ObjectForTestStream.class); - eventStreamManager.addEvent(event); + final ObjectForTestStream event = mock(ObjectForTestStream.class); + eventStreamManager.addEvents(List.of(event)); + verify(multiStreamMock).addObject(event); // for non-freeze event, multiStream should not be closed after adding it verify(multiStreamMock, never()).close(); - assertFalse( - eventStreamManager.getFreezePeriodStarted(), - "freezePeriodStarted should be false after adding non-freeze event"); } - eventStreamManager.addEvent(freezeEvent); + + eventStreamManager.addEvents(List.of(freezeEvent)); verify(multiStreamMock).addObject(freezeEvent); // for freeze event, multiStream should be closed after adding it verify(multiStreamMock).close(); - assertTrue( - eventStreamManager.getFreezePeriodStarted(), "freezePeriodStarted should be true adding freeze event"); - ObjectForTestStream eventAddAfterFrozen = mock(ObjectForTestStream.class); - eventStreamManager.addEvent(eventAddAfterFrozen); + final ObjectForTestStream eventAddAfterFrozen = mock(ObjectForTestStream.class); + eventStreamManager.addEvents(List.of(eventAddAfterFrozen)); // after frozen, when adding event to the EventStreamManager, multiStream.add(event) should not be called verify(multiStreamMock, never()).addObject(eventAddAfterFrozen); } @ParameterizedTest @ValueSource(booleans = {true, false}) - void setStartWriteAtCompleteWindowTest(boolean startWriteAtCompleteWindow) { - enableStreamingInstance.setStartWriteAtCompleteWindow(startWriteAtCompleteWindow); - assertEquals( - startWriteAtCompleteWindow, - enableStreamingInstance.getStreamFileWriter().getStartWriteAtCompleteWindow(), - UNEXPECTED_VALUE); + void setStartWriteAtCompleteWindowTest(final boolean startWriteAtCompleteWindow) { + final Random random = RandomUtils.getRandomPrintSeed(); + final Hash runningHash = RandomUtils.randomHash(random); + eventStreamManager.updateRunningHash(new RunningEventHashUpdate(runningHash, startWriteAtCompleteWindow)); + verify(multiStreamMock).setRunningHash(runningHash); } /** * used for testing adding freeze event * - * @param event - * the event to be added + * @param event the event to be added * @return whether */ private static boolean isFreezeEvent(final ObjectForTestStream event) { diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/locks/IndexLockTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/locks/IndexLockTests.java index bbcec996628e..f8b03d63ecc9 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/locks/IndexLockTests.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/locks/IndexLockTests.java @@ -24,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.swirlds.common.test.fixtures.AssertionUtils; import com.swirlds.common.test.fixtures.junit.tags.TestComponentTags; import com.swirlds.common.threading.framework.config.ThreadConfiguration; import com.swirlds.common.threading.locks.locked.Locked; @@ -78,10 +79,8 @@ void sameIndexBlocks() throws InterruptedException { lock.unlock(object); - // Give the thread time to acquire the lock if it can - MILLISECONDS.sleep(20); - - assertTrue(threadIsLocked.get(), "thread should have been able to acquire lock"); + AssertionUtils.assertEventuallyTrue( + threadIsLocked::get, Duration.ofSeconds(1), "thread should have been able to acquire lock"); } /** diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/wires/OutputWireTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/wires/OutputWireTests.java new file mode 100644 index 000000000000..5ee65c7c23ee --- /dev/null +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/wiring/wires/OutputWireTests.java @@ -0,0 +1,125 @@ +/* + * 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.wires; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.swirlds.base.time.Time; +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.BindableInputWire; +import com.swirlds.common.wiring.wires.input.InputWire; +import java.util.List; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Tests the functionality of output wires + */ +public class OutputWireTests { + + /** + * Test that the ordered solder to method forwards data in the proper order. + * + * @param count the number of data to send through the wires + */ + @ParameterizedTest() + @ValueSource(ints = {10_000}) + void orderedSolderToTest(final int count) { + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); + final WiringModel model = WiringModel.create(platformContext, Time.getCurrent(), ForkJoinPool.commonPool()); + + final TaskScheduler intForwarder = model.schedulerBuilder("intForwarder") + .withType(TaskSchedulerType.DIRECT) + .build() + .cast(); + final TaskScheduler firstComponent = model.schedulerBuilder("firstComponent") + .withType(TaskSchedulerType.DIRECT) + .build() + .cast(); + final TaskScheduler secondComponent = model.schedulerBuilder("secondComponent") + .withType(TaskSchedulerType.DIRECT) + .build() + .cast(); + + final BindableInputWire intInput = intForwarder.buildInputWire("intInput"); + final BindableInputWire firstComponentInput = firstComponent.buildInputWire("ints"); + final BindableInputWire secondComponentInput = secondComponent.buildInputWire("ints"); + + // Send integers to the first component before the second component + final List> inputList = List.of(firstComponentInput, secondComponentInput); + intForwarder.getOutputWire().orderedSolderTo(inputList); + + intInput.bind((i -> i)); + + final AtomicInteger firstCompRecNum = new AtomicInteger(); + final AtomicInteger secondCompRecNum = new AtomicInteger(); + final AtomicInteger firstCompErrorCount = new AtomicInteger(); + final AtomicInteger secondCompErrorCount = new AtomicInteger(); + + firstComponentInput.bind(i -> { + if (firstCompRecNum.incrementAndGet() <= secondCompRecNum.get()) { + firstCompErrorCount.incrementAndGet(); + } + }); + secondComponentInput.bind(i -> { + if (firstCompRecNum.get() != secondCompRecNum.incrementAndGet()) { + secondCompErrorCount.incrementAndGet(); + } + }); + + for (int i = 0; i < count; i++) { + intInput.put(i); + } + + assertEquals(0, firstCompErrorCount.get(), "The first component should always receive data first"); + assertEquals(0, secondCompErrorCount.get(), "The second component should always receive data second"); + } + + /** + * Test that the expected exceptions are thrown + */ + @Test + void orderedSolderToThrows() { + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); + final WiringModel model = WiringModel.create(platformContext, Time.getCurrent(), ForkJoinPool.commonPool()); + + final TaskScheduler schedulerA = model.schedulerBuilder("schedulerA") + .withType(TaskSchedulerType.DIRECT) + .build() + .cast(); + final TaskScheduler schedulerB = model.schedulerBuilder("schedulerB") + .withType(TaskSchedulerType.DIRECT) + .build() + .cast(); + + InputWire inputWire = schedulerB.buildInputWire("inputWire"); + assertThrows( + IllegalArgumentException.class, + () -> schedulerA.getOutputWire().orderedSolderTo(List.of(inputWire)), + "Method should throw when provided less than two input wires."); + } +} diff --git a/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/reconnect/VirtualMapReconnectTestBase.java b/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/reconnect/VirtualMapReconnectTestBase.java index 245325e8c584..4f50c12e8254 100644 --- a/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/reconnect/VirtualMapReconnectTestBase.java +++ b/platform-sdk/swirlds-merkle/src/test/java/com/swirlds/virtual/merkle/reconnect/VirtualMapReconnectTestBase.java @@ -225,6 +225,9 @@ protected void reconnectMultipleTimes( final VirtualRoot root = learnerMap.getRight(); assertTrue(root.isHashed(), "Learner root node must be hashed"); } catch (Exception e) { + if (!failureExpected) { + e.printStackTrace(System.err); + } assertTrue(failureExpected, "We did not expect an exception on this reconnect attempt! " + e); } diff --git a/platform-sdk/swirlds-platform-core/build.gradle.kts b/platform-sdk/swirlds-platform-core/build.gradle.kts index d71a882ed2b1..1f345fc4c399 100644 --- a/platform-sdk/swirlds-platform-core/build.gradle.kts +++ b/platform-sdk/swirlds-platform-core/build.gradle.kts @@ -44,7 +44,6 @@ testModuleInfo { requires("com.swirlds.platform.core") requires("com.swirlds.config.extensions.test.fixtures") requires("org.assertj.core") - requires("awaitility") requires("org.junit.jupiter.api") requires("org.junit.jupiter.params") requires("org.mockito") 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 23f9f75c56d9..d9f06cf30d37 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 @@ -25,7 +25,7 @@ import static com.swirlds.platform.event.creation.EventCreationManagerFactory.buildEventCreationManager; import static com.swirlds.platform.event.preconsensus.PcesUtilities.getDatabaseDirectory; import static com.swirlds.platform.state.address.AddressBookMetrics.registerAddressBookMetrics; -import static com.swirlds.platform.state.iss.ConsensusHashManager.DO_NOT_IGNORE_ROUNDS; +import static com.swirlds.platform.state.iss.IssDetector.DO_NOT_IGNORE_ROUNDS; import static com.swirlds.platform.state.signed.SignedStateFileReader.getSavedStateFiles; import static com.swirlds.platform.system.InitTrigger.GENESIS; import static com.swirlds.platform.system.InitTrigger.RESTART; @@ -46,6 +46,7 @@ import com.swirlds.common.platform.NodeId; import com.swirlds.common.scratchpad.Scratchpad; import com.swirlds.common.stream.EventStreamManager; +import com.swirlds.common.stream.RunningEventHashUpdate; import com.swirlds.common.threading.framework.QueueThread; import com.swirlds.common.threading.framework.config.QueueThreadConfiguration; import com.swirlds.common.threading.framework.config.QueueThreadMetricsConfiguration; @@ -55,6 +56,7 @@ import com.swirlds.common.utility.Clearable; import com.swirlds.common.utility.LoggingClearables; import com.swirlds.common.utility.StackTrace; +import com.swirlds.common.wiring.wires.output.OutputWire; import com.swirlds.logging.legacy.LogMarker; import com.swirlds.logging.legacy.payload.FatalErrorPayload; import com.swirlds.metrics.api.Metrics; @@ -63,7 +65,6 @@ import com.swirlds.platform.components.appcomm.AppCommunicationComponent; import com.swirlds.platform.components.state.DefaultStateManagementComponent; import com.swirlds.platform.components.state.StateManagementComponent; -import com.swirlds.platform.components.transaction.system.ConsensusSystemTransactionManager; import com.swirlds.platform.config.StateConfig; import com.swirlds.platform.config.ThreadConfig; import com.swirlds.platform.config.TransactionConfig; @@ -74,8 +75,6 @@ import com.swirlds.platform.crypto.PlatformSigner; import com.swirlds.platform.dispatch.DispatchBuilder; import com.swirlds.platform.dispatch.DispatchConfiguration; -import com.swirlds.platform.dispatch.triggers.flow.DiskStateLoadedTrigger; -import com.swirlds.platform.dispatch.triggers.flow.ReconnectStateLoadedTrigger; import com.swirlds.platform.event.AncientMode; import com.swirlds.platform.event.EventCounter; import com.swirlds.platform.event.FutureEventBuffer; @@ -116,11 +115,9 @@ import com.swirlds.platform.listeners.StateLoadedFromDiskCompleteListener; import com.swirlds.platform.listeners.StateLoadedFromDiskNotification; import com.swirlds.platform.metrics.AddedEventMetrics; -import com.swirlds.platform.metrics.ConsensusHandlingMetrics; import com.swirlds.platform.metrics.ConsensusMetrics; import com.swirlds.platform.metrics.ConsensusMetricsImpl; import com.swirlds.platform.metrics.EventIntakeMetrics; -import com.swirlds.platform.metrics.IssMetrics; import com.swirlds.platform.metrics.RuntimeMetrics; import com.swirlds.platform.metrics.SwirldStateMetrics; import com.swirlds.platform.metrics.SyncMetrics; @@ -130,7 +127,7 @@ import com.swirlds.platform.recovery.EmergencyRecoveryManager; import com.swirlds.platform.state.State; import com.swirlds.platform.state.SwirldStateManager; -import com.swirlds.platform.state.iss.ConsensusHashManager; +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.EmergencyStateNexus; @@ -159,14 +156,18 @@ 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.state.notifications.IssListener; +import com.swirlds.platform.system.state.notifications.IssNotification; +import com.swirlds.platform.system.state.notifications.IssNotification.IssType; import com.swirlds.platform.system.status.PlatformStatus; import com.swirlds.platform.system.status.PlatformStatusManager; +import com.swirlds.platform.system.status.actions.CatastrophicFailureAction; import com.swirlds.platform.system.status.actions.DoneReplayingEventsAction; import com.swirlds.platform.system.status.actions.ReconnectCompleteAction; import com.swirlds.platform.system.status.actions.StartedReplayingEventsAction; -import com.swirlds.platform.system.transaction.StateSignatureTransaction; import com.swirlds.platform.system.transaction.SwirldTransaction; import com.swirlds.platform.util.PlatformComponents; +import com.swirlds.platform.wiring.NoInput; import com.swirlds.platform.wiring.PlatformWiring; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -176,6 +177,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -234,9 +236,6 @@ public class SwirldsPlatform implements Platform { */ private final SignedStateNexus latestImmutableState = new LockFreeStateNexus(); - /** Stores and processes consensus events including sending them to {@link SwirldStateManager} for handling */ - private final ConsensusRoundHandler consensusRoundHandler; - private final TransactionPool transactionPool; /** Handles all interaction with {@link SwirldState} */ private final SwirldStateManager swirldStateManager; @@ -250,15 +249,6 @@ public class SwirldsPlatform implements Platform { */ private final PlatformComponents components; - /** - * Call this when a reconnect has been completed. - */ - private final ReconnectStateLoadedTrigger reconnectStateLoadedDispatcher; - - /** - * Call this when a state has been loaded from disk. - */ - private final DiskStateLoadedTrigger diskStateLoadedDispatcher; /** * For passing notifications between the platform and the application. */ @@ -290,8 +280,6 @@ public class SwirldsPlatform implements Platform { */ private final AtomicLong latestReconnectRound = new AtomicLong(NO_ROUND); - final ConsensusHashManager consensusHashManager; - /** Manages emergency recovery */ private final EmergencyRecoveryManager emergencyRecoveryManager; /** Controls which states are saved to disk */ @@ -301,6 +289,8 @@ public class SwirldsPlatform implements Platform { * Encapsulated wiring for the platform. */ private final PlatformWiring platformWiring; + /** thread-queue responsible for hashing states */ + private final QueueThread stateHashSignQueue; /** * the browser gives the Platform what app to run. There can be multiple Platforms on one computer. @@ -344,10 +334,6 @@ public class SwirldsPlatform implements Platform { dispatchBuilder.registerObservers(this); - reconnectStateLoadedDispatcher = - dispatchBuilder.getDispatcher(this, ReconnectStateLoadedTrigger.class)::dispatch; - diskStateLoadedDispatcher = dispatchBuilder.getDispatcher(this, DiskStateLoadedTrigger.class)::dispatch; - final StateConfig stateConfig = platformContext.getConfiguration().getConfigData(StateConfig.class); final String actualMainClassName = stateConfig.getMainClassName(mainClassName); @@ -474,26 +460,13 @@ public class SwirldsPlatform implements Platform { // without a software upgrade (in production this feature should not be used). final long roundToIgnore = stateConfig.validateInitialState() ? DO_NOT_IGNORE_ROUNDS : initialState.getRound(); - consensusHashManager = components.add(new ConsensusHashManager( + final IssDetector issDetector = new IssDetector( platformContext, - Time.getCurrent(), - dispatchBuilder, currentAddressBook, epochHash, appVersion, ignorePreconsensusSignatures, - roundToIgnore)); - - components.add(new IssHandler( - stateConfig, - selfId, - platformStatusManager, - this::haltRequested, - this::handleFatalError, - appCommunicationComponent, - issScratchpad)); - - components.add(new IssMetrics(platformContext.getMetrics(), currentAddressBook)); + roundToIgnore); final SignedStateFileManager signedStateFileManager = new SignedStateFileManager( platformContext, @@ -518,7 +491,6 @@ public class SwirldsPlatform implements Platform { stateManagementComponent = new DefaultStateManagementComponent( platformContext, threadManager, - dispatchBuilder, this::handleFatalError, platformWiring.getSignStateInput()::put, platformWiring.getSignatureCollectorStateInput()::put, @@ -530,20 +502,14 @@ public class SwirldsPlatform implements Platform { time, platformWiring.getPcesReplayerEventOutput(), platformWiring::flushIntakePipeline, - this::waitUntilTransactionHandlingThreadIsNotBusy, + platformWiring::flushConsensusRoundHandler, () -> latestImmutableState.getState("PCES replay")); final EventDurabilityNexus eventDurabilityNexus = new EventDurabilityNexus(); components.add(stateManagementComponent); - final ConsensusSystemTransactionManager consensusSystemTransactionManager = - new ConsensusSystemTransactionManager(); - consensusSystemTransactionManager.addHandler( - StateSignatureTransaction.class, - (ignored, nodeId, txn, v) -> - consensusHashManager.handlePostconsensusSignatureTransaction(nodeId, txn, v)); final BiConsumer roundAndStateConsumer = (state, round) -> { - consensusSystemTransactionManager.handleRound(state, round); + platformWiring.getIssDetectorWiring().handleConsensusRound().put(round); platformWiring.getSignatureCollectorConsensusInput().put(round); }; @@ -591,37 +557,34 @@ public class SwirldsPlatform implements Platform { latestImmutableState.setState(rs.getAndReserve("newSignedStateFromTransactionsConsumer")); latestCompleteState.newIncompleteState(rs.get().getRound()); savedStateController.markSavedState(rs.getAndReserve("savedStateController.markSavedState")); - stateManagementComponent.newSignedStateFromTransactions(rs); + stateManagementComponent.newSignedStateFromTransactions( + rs.getAndReserve("stateManagementComponent.newSignedStateFromTransactions")); + platformWiring.getIssDetectorWiring().newStateHashed().put(rs.getAndReserve("issDetector")); + rs.close(); }; - final QueueThread stateHashSignQueue = - components.add(new QueueThreadConfiguration(threadManager) - .setNodeId(selfId) - .setComponent(PLATFORM_THREAD_POOL_NAME) - .setThreadName("state_hash_sign") - .setHandler(newSignedStateFromTransactionsConsumer) - .setCapacity(1) - .setMetricsConfiguration(new QueueThreadMetricsConfiguration(metrics).enableBusyTimeMetric()) - .build()); - - consensusRoundHandler = components.add(new ConsensusRoundHandler( + stateHashSignQueue = components.add(new QueueThreadConfiguration(threadManager) + .setNodeId(selfId) + .setComponent(PLATFORM_THREAD_POOL_NAME) + .setThreadName("state_hash_sign") + .setHandler(newSignedStateFromTransactionsConsumer) + .setCapacity(1) + .setMetricsConfiguration(new QueueThreadMetricsConfiguration(metrics).enableBusyTimeMetric()) + .build()); + + final ConsensusRoundHandler consensusRoundHandler = new ConsensusRoundHandler( platformContext, - threadManager, - selfId, swirldStateManager, - new ConsensusHandlingMetrics(metrics, time), - eventStreamManager, stateHashSignQueue, eventDurabilityNexus::waitUntilDurable, platformStatusManager, - consensusHashManager::roundCompleted, - appVersion)); + platformWiring.getIssDetectorWiring().roundCompletedInput()::put, + appVersion); final AddedEventMetrics addedEventMetrics = new AddedEventMetrics(this.selfId, metrics); final PcesSequencer sequencer = new PcesSequencer(); - final List eventObservers = - new ArrayList<>(List.of(consensusRoundHandler, addedEventMetrics, eventIntakeMetrics)); + final List eventObservers = new ArrayList<>(List.of(addedEventMetrics, eventIntakeMetrics)); final EventObserverDispatcher eventObserverDispatcher = new EventObserverDispatcher(eventObservers); @@ -648,8 +611,6 @@ public class SwirldsPlatform implements Platform { final OrphanBuffer orphanBuffer = new OrphanBuffer(platformContext, intakeEventCounter); final InOrderLinker inOrderLinker = new InOrderLinker(platformContext, time, intakeEventCounter); final LinkedEventIntake linkedEventIntake = new LinkedEventIntake( - platformContext, - time, consensusRef::get, eventObserverDispatcher, shadowGraph, @@ -664,7 +625,7 @@ public class SwirldsPlatform implements Platform { selfId, appVersion, transactionPool, - platformWiring.getHasherUnprocessedTaskCountSupplier(), + platformWiring.getIntakeQueueSizeSupplier(), platformStatusManager::getCurrentStatus, latestReconnectRound::get); @@ -673,6 +634,19 @@ public class SwirldsPlatform implements Platform { final FutureEventBuffer futureEventBuffer = new FutureEventBuffer(platformContext); + // wire ISS output + final IssHandler issHandler = + new IssHandler(stateConfig, this::haltRequested, this::handleFatalError, issScratchpad); + final OutputWire issOutput = + platformWiring.getIssDetectorWiring().issNotificationOutput(); + issOutput.solderTo("issNotificationEngine", n -> notificationEngine.dispatch(IssListener.class, n)); + issOutput.solderTo("statusManager", n -> { + if (Set.of(IssType.SELF_ISS, IssType.CATASTROPHIC_ISS).contains(n.getIssType())) { + platformStatusManager.submitStatusAction(new CatastrophicFailureAction()); + } + }); + issOutput.solderTo("issHandler", issHandler::issObserved); + platformWiring.bind( eventHasher, internalEventValidator, @@ -691,7 +665,10 @@ public class SwirldsPlatform implements Platform { eventCreationManager, swirldStateManager, stateSignatureCollector, - futureEventBuffer); + consensusRoundHandler, + eventStreamManager, + futureEventBuffer, + issDetector); // Load the minimum generation into the pre-consensus event writer final List savedStates = @@ -725,7 +702,7 @@ public class SwirldsPlatform implements Platform { emergencyRecoveryManager, consensusRef, platformWiring.getGossipEventInput()::put, - platformWiring.getHasherUnprocessedTaskCountSupplier(), + platformWiring.getIntakeQueueSizeSupplier(), swirldStateManager, latestCompleteState, syncMetrics, @@ -755,7 +732,8 @@ public class SwirldsPlatform implements Platform { latestImmutableState.setState(initialState.reserve("set latest immutable to initial state")); stateManagementComponent.stateToLoad(initialState, SourceOfSignedState.DISK); savedStateController.registerSignedStateFromDisk(initialState); - consensusRoundHandler.loadDataFromSignedState(initialState, false); + + platformWiring.updateRunningHash(new RunningEventHashUpdate(initialState.getHashEventsCons(), false)); loadStateIntoConsensus(initialState); @@ -767,28 +745,31 @@ public class SwirldsPlatform implements Platform { initialMinimumGenerationNonAncient, initialMinimumGenerationNonAncient, AncientMode.getAncientMode(platformContext))); + platformWiring.getIssDetectorWiring().overridingState().put(initialState.reserve("initialize issDetector")); // We don't want to invoke these callbacks until after we are starting up. - final long round = initialState.getRound(); - final Hash hash = initialState.getState().getHash(); components.add((Startable) () -> { // If we loaded from disk then call the appropriate dispatch. - // It is important that this is sent after the ConsensusHashManager - // is initialized. - diskStateLoadedDispatcher.dispatch(round, hash); - // Let the app know that a state was loaded. notificationEngine.dispatch( StateLoadedFromDiskCompleteListener.class, new StateLoadedFromDiskNotification()); }); } + final Clearable clearStateHashSignQueue = () -> { + ReservedSignedState signedState = stateHashSignQueue.poll(); + while (signedState != null) { + signedState.close(); + signedState = stateHashSignQueue.poll(); + } + }; + clearAllPipelines = new LoggingClearables( RECONNECT.getMarker(), List.of( Pair.of(platformWiring, "platformWiring"), Pair.of(shadowGraph, "shadowGraph"), - Pair.of(consensusRoundHandler, "consensusRoundHandler"), + Pair.of(clearStateHashSignQueue, "stateHashSignQueue"), Pair.of(transactionPool, "transactionPool"))); if (platformContext.getConfiguration().getConfigData(ThreadConfig.class).jvmAnchor()) { @@ -802,18 +783,6 @@ public class SwirldsPlatform implements Platform { GuiPlatformAccessor.getInstance().setLatestImmutableStateComponent(selfId, latestImmutableState); } - /** - * Wait until the consensus round handler is not busy. - */ - private void waitUntilTransactionHandlingThreadIsNotBusy() { - try { - consensusRoundHandler.waitUntilNotBusy(); - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Interrupted waiting for transaction handling thread to not be busy", e); - } - } - /** * Clears all pipelines in preparation for a reconnect. This method is needed to break a circular dependency. */ @@ -908,9 +877,10 @@ private void loadReconnectState(final SignedState signedState) { logger.info(LogMarker.STATE_HASH.getMarker(), "RECONNECT: loadReconnectState: reloading state"); logger.debug(RECONNECT.getMarker(), "`loadReconnectState` : reloading state"); try { - - reconnectStateLoadedDispatcher.dispatch( - signedState.getRound(), signedState.getState().getHash()); + platformWiring + .getIssDetectorWiring() + .overridingState() + .put(signedState.reserve("reconnect state to issDetector")); // It's important to call init() before loading the signed state. The loading process makes copies // of the state, and we want to be sure that the first state in the chain of copies has been initialized. @@ -959,8 +929,7 @@ private void loadReconnectState(final SignedState signedState) { signedState.getMinRoundGeneration(), AncientMode.getAncientMode(platformContext))); - consensusRoundHandler.loadDataFromSignedState(signedState, true); - + platformWiring.updateRunningHash(new RunningEventHashUpdate(signedState.getHashEventsCons(), true)); platformWiring.getPcesWriterRegisterDiscontinuityInput().inject(signedState.getRound()); // Notify any listeners that the reconnect has been completed @@ -1058,7 +1027,16 @@ private void replayPreconsensusEvents() { platformWiring.getPcesReplayerIteratorInput().inject(iterator); } - consensusHashManager.signalEndOfPreconsensusReplay(); + // we have to wait for all the PCES transactions to reach the ISS detector before telling it that PCES replay is + // done the PCES replay will flush the intake pipeline, so we have to flush the hasher + try { + stateHashSignQueue.waitUntilNotBusy(); + } catch (final InterruptedException e) { + throw new RuntimeException(e); + } + // FUTURE WORK: once the state hasher is moved to the platform wiring, this flush can be done by the PCES + // replayer. the same goes for the flush of the state hasher + platformWiring.getIssDetectorWiring().endOfPcesReplay().put(NoInput.getInstance()); platformStatusManager.submitStatusAction( new DoneReplayingEventsAction(Time.getCurrent().now())); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/LinkedEventIntake.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/LinkedEventIntake.java index 1295cec88fbd..90d3b3921ea2 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/LinkedEventIntake.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/LinkedEventIntake.java @@ -16,8 +16,6 @@ package com.swirlds.platform.components; -import com.swirlds.base.time.Time; -import com.swirlds.common.context.PlatformContext; import com.swirlds.common.wiring.wires.output.StandardOutputWire; import com.swirlds.platform.Consensus; import com.swirlds.platform.gossip.IntakeEventCounter; @@ -50,9 +48,6 @@ public class LinkedEventIntake { */ private final Shadowgraph shadowGraph; - private final EventIntakeMetrics metrics; - private final Time time; - /** * Tracks the number of events from each peer have been received, but aren't yet through the intake pipeline */ @@ -73,22 +68,18 @@ public class LinkedEventIntake { /** * Constructor * - * @param platformContext the platform context - * @param time provides the wall clock time - * @param consensusSupplier provides the current consensus instance - * @param dispatcher invokes event related callbacks - * @param shadowGraph tracks events in the hashgraph + * @param consensusSupplier provides the current consensus instance + * @param dispatcher invokes event related callbacks + * @param shadowGraph tracks events in the hashgraph * @param keystoneEventSequenceNumberOutput the secondary wire that outputs the keystone event sequence number */ public LinkedEventIntake( - @NonNull final PlatformContext platformContext, - @NonNull final Time time, @NonNull final Supplier consensusSupplier, @NonNull final EventObserverDispatcher dispatcher, @NonNull final Shadowgraph shadowGraph, @NonNull final IntakeEventCounter intakeEventCounter, @NonNull final StandardOutputWire keystoneEventSequenceNumberOutput) { - this.time = Objects.requireNonNull(time); + this.consensusSupplier = Objects.requireNonNull(consensusSupplier); this.dispatcher = Objects.requireNonNull(dispatcher); this.shadowGraph = Objects.requireNonNull(shadowGraph); @@ -96,7 +87,6 @@ public LinkedEventIntake( this.keystoneEventSequenceNumberOutput = Objects.requireNonNull(keystoneEventSequenceNumberOutput); this.paused = false; - metrics = new EventIntakeMetrics(platformContext, () -> -1); } /** @@ -136,7 +126,9 @@ public List addEvent(@NonNull final EventImpl event) { // PCES writer hasn't been notified yet that the event should be flushed. keystoneEventSequenceNumberOutput.forward( round.getKeystoneEvent().getBaseEvent().getStreamSequenceNumber()); - handleConsensus(round); + // Future work: this dispatcher now only handles metrics. Remove this and put the metrics where + // they belong + dispatcher.consensusRound(round); }); } @@ -190,21 +182,4 @@ private void handleStale(final long previousGenerationNonAncient) { private static boolean isNotConsensus(@NonNull final EventImpl event) { return !event.isConsensus(); } - - /** - * Notify observers that an event has reach consensus. - * - * @param consensusRound the new consensus round - */ - private void handleConsensus(final @NonNull ConsensusRound consensusRound) { - // We need to wait for prehandles to finish before proceeding. - // It is critically important that prehandle is always called prior to handleConsensusRound(). - - final long start = time.nanoTime(); - consensusRound.forEach(event -> ((EventImpl) event).getBaseEvent().awaitPrehandleCompletion()); - final long end = time.nanoTime(); - metrics.reportTimeWaitedForPrehandlingTransaction(end - start); - - dispatcher.consensusRound(consensusRound); - } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/appcomm/AppCommunicationComponent.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/appcomm/AppCommunicationComponent.java index 34eea9b2dfa2..8bededb9de3d 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/appcomm/AppCommunicationComponent.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/appcomm/AppCommunicationComponent.java @@ -23,11 +23,9 @@ import com.swirlds.common.context.PlatformContext; import com.swirlds.common.notification.NotificationEngine; -import com.swirlds.common.platform.NodeId; import com.swirlds.common.threading.framework.QueueThread; import com.swirlds.common.threading.framework.config.QueueThreadConfiguration; import com.swirlds.platform.components.PlatformComponent; -import com.swirlds.platform.components.state.output.IssConsumer; import com.swirlds.platform.consensus.ConsensusConstants; import com.swirlds.platform.listeners.StateWriteToDiskCompleteListener; import com.swirlds.platform.listeners.StateWriteToDiskCompleteNotification; @@ -35,19 +33,16 @@ import com.swirlds.platform.state.signed.StateSavingResult; import com.swirlds.platform.stats.AverageAndMax; import com.swirlds.platform.stats.AverageStat; -import com.swirlds.platform.system.state.notifications.IssListener; -import com.swirlds.platform.system.state.notifications.IssNotification; import com.swirlds.platform.system.state.notifications.NewSignedStateListener; import com.swirlds.platform.system.state.notifications.NewSignedStateNotification; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * This component responsible for notifying the application of various platform events */ -public class AppCommunicationComponent implements PlatformComponent, IssConsumer { +public class AppCommunicationComponent implements PlatformComponent { private static final Logger logger = LogManager.getLogger(AppCommunicationComponent.class); private final NotificationEngine notificationEngine; @@ -136,13 +131,6 @@ private void latestCompleteStateHandler(@NonNull final ReservedSignedState reser notificationEngine.dispatch(NewSignedStateListener.class, notification, r -> reservedSignedState.close()); } - @Override - public void iss( - final long round, @NonNull final IssNotification.IssType issType, @Nullable final NodeId otherNodeId) { - final IssNotification notification = new IssNotification(round, issType, otherNodeId); - notificationEngine.dispatch(IssListener.class, notification); - } - /** * Update the size of the task queue for * {@link com.swirlds.platform.components.state.output.NewLatestCompleteStateConsumer}s 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 75925b3fbf6e..f6edd44f8074 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 @@ -21,8 +21,6 @@ import com.swirlds.common.threading.manager.ThreadManager; import com.swirlds.platform.components.common.output.FatalErrorConsumer; import com.swirlds.platform.config.StateConfig; -import com.swirlds.platform.dispatch.DispatchBuilder; -import com.swirlds.platform.dispatch.triggers.flow.StateHashedTrigger; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.state.signed.SignedStateGarbageCollector; @@ -67,7 +65,6 @@ public class DefaultStateManagementComponent implements StateManagementComponent /** * @param platformContext the platform context * @param threadManager manages platform thread resources - * @param dispatchBuilder builds dispatchers. This is deprecated, do not wire new things together with this. * @param fatalErrorConsumer consumer to invoke when a fatal error has occurred * @param stateSigner signs a state * @param sigCollector collects signatures for a state @@ -76,7 +73,6 @@ public class DefaultStateManagementComponent implements StateManagementComponent public DefaultStateManagementComponent( @NonNull final PlatformContext platformContext, @NonNull final ThreadManager threadManager, - @NonNull final DispatchBuilder dispatchBuilder, @NonNull final FatalErrorConsumer fatalErrorConsumer, @NonNull final Consumer stateSigner, @NonNull final Consumer sigCollector, @@ -96,9 +92,7 @@ public DefaultStateManagementComponent( hashLogger = new HashLogger(threadManager, platformContext.getConfiguration().getConfigData(StateConfig.class)); - final StateHashedTrigger stateHashedTrigger = - dispatchBuilder.getDispatcher(this, StateHashedTrigger.class)::dispatch; - signedStateHasher = new SignedStateHasher(signedStateMetrics, stateHashedTrigger, fatalErrorConsumer); + signedStateHasher = new SignedStateHasher(signedStateMetrics, fatalErrorConsumer); } private void logHashes(final SignedState signedState) { diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/state/output/IssConsumer.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/state/output/IssConsumer.java deleted file mode 100644 index 839669febb0c..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/state/output/IssConsumer.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.components.state.output; - -import com.swirlds.common.platform.NodeId; -import com.swirlds.platform.system.state.notifications.IssNotification; -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; - -/** - * Invoked when an Invalid State Signature (ISS) is detected. - */ -@FunctionalInterface -public interface IssConsumer { - - /** - * An ISS has occurred. - * - * @param round - * the round of the ISS - * @param issType - * the type of ISS - * @param issNodeId - * the id of the node with the ISS, or {@code null} if it is a catastrophic ISS - */ - void iss(long round, @NonNull IssNotification.IssType issType, @Nullable NodeId issNodeId); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/transaction/system/ConsensusSystemTransactionManager.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/transaction/system/ConsensusSystemTransactionManager.java deleted file mode 100644 index d25a4a78de5b..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/transaction/system/ConsensusSystemTransactionManager.java +++ /dev/null @@ -1,129 +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.components.transaction.system; - -import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; - -import com.swirlds.common.platform.NodeId; -import com.swirlds.platform.internal.ConsensusRound; -import com.swirlds.platform.internal.EventImpl; -import com.swirlds.platform.state.State; -import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.transaction.SystemTransaction; -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * Routes consensus system transactions to the appropriate handlers. - */ -public class ConsensusSystemTransactionManager { - - /** - * Class logger - */ - private static final Logger logger = LogManager.getLogger(ConsensusSystemTransactionManager.class); - - /** - * The post-consensus handle methods that have been registered - */ - private final Map, List>> handlers = new HashMap<>(); - - /** - * Add a handle method - * - * @param clazz the class of the transaction being handled - * @param handler a method to handle this transaction type - */ - @SuppressWarnings("unchecked") - public void addHandler( - @NonNull final Class clazz, @NonNull final ConsensusSystemTransactionHandler handler) { - - Objects.requireNonNull(clazz); - Objects.requireNonNull(handler); - - handlers.computeIfAbsent(clazz, k -> new ArrayList<>()) - .add((ConsensusSystemTransactionHandler) handler); - } - - /** - * Pass an individual transaction to all handlers that want it - * - * @param state the state - * @param creatorId the id of the creator of the transaction - * @param transaction the transaction being handled - * @param eventVersion the version of the event that contains the transaction - */ - private void handleTransaction( - @NonNull final State state, - @NonNull final NodeId creatorId, - @NonNull final SystemTransaction transaction, - @Nullable SoftwareVersion eventVersion) { - Objects.requireNonNull(creatorId, "creatorId must not be null"); - Objects.requireNonNull(transaction, "transaction must not be null"); - - final List> relevantHandlers = - handlers.get(transaction.getClass()); - - if (relevantHandlers == null) { - // no handlers exist that want this transaction type - return; - } - - for (final ConsensusSystemTransactionHandler handler : relevantHandlers) { - try { - handler.handle(state, creatorId, transaction, eventVersion); - } catch (final RuntimeException e) { - logger.error( - EXCEPTION.getMarker(), - "Error while handling system transaction post consensus: " - + "handler: {}, id: {}, transaction: {}, error: {}", - handler, - creatorId, - transaction, - e); - } - } - } - - /** - * Handle a post-consensus round by passing each included system transaction to the registered handlers - * - * @param state a mutable state - * @param round the post-consensus round - */ - public void handleRound(@NonNull final State state, @NonNull final ConsensusRound round) { - // no post-consensus handling methods have been registered - if (handlers.isEmpty()) { - return; - } - - Objects.requireNonNull(state); - - for (final EventImpl event : round.getConsensusEvents()) { - event.systemTransactionIterator() - .forEachRemaining(transaction -> - handleTransaction(state, event.getCreatorId(), transaction, event.getSoftwareVersion())); - } - } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/transaction/system/ScopedSystemTransaction.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/transaction/system/ScopedSystemTransaction.java index b591952c0ca5..3cd611c1afc4 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/transaction/system/ScopedSystemTransaction.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/components/transaction/system/ScopedSystemTransaction.java @@ -17,17 +17,20 @@ package com.swirlds.platform.components.transaction.system; import com.swirlds.common.platform.NodeId; +import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.transaction.SystemTransaction; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; /** - * A system transaction with a submitter ID. The submitter ID is not included with the transaction, it is determined - * by the event that the transaction is contained within. This is intentional, as it makes it impossible for a - * transaction to lie and claim to be submitted by a node that did not actually submit it. + * A system transaction with a submitter ID and a software version. The submitter ID is not included with the + * transaction, it is determined by the event that the transaction is contained within. This is intentional, as it makes + * it impossible for a transaction to lie and claim to be submitted by a node that did not actually submit it. * - * @param submitterId the ID of the node that submitted the transaction - * @param transaction the transaction - * @param the type of transaction + * @param submitterId the ID of the node that submitted the transaction + * @param softwareVersion the software version of the event that contained the transaction + * @param transaction the transaction + * @param the type of transaction */ public record ScopedSystemTransaction( - @NonNull NodeId submitterId, @NonNull T transaction) {} + @NonNull NodeId submitterId, @Nullable SoftwareVersion softwareVersion, @NonNull T transaction) {} 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/SystemTransactionExtractor.java index 3ad80cc4952a..a8202c08f506 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/SystemTransactionExtractor.java @@ -80,8 +80,10 @@ public SystemTransactionExtractor(@NonNull final Class systemTransactionType) for (final Transaction transaction : event.getHashedData().getTransactions()) { if (systemTransactionType.isInstance(transaction)) { - scopedTransactions.add( - new ScopedSystemTransaction<>(event.getHashedData().getCreatorId(), (T) transaction)); + scopedTransactions.add(new ScopedSystemTransaction<>( + event.getHashedData().getCreatorId(), + event.getHashedData().getSoftwareVersion(), + (T) transaction)); } } return scopedTransactions.isEmpty() ? null : scopedTransactions; diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/config/StateConfig.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/config/StateConfig.java index 644dc6c1625f..3ad109c034a4 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/config/StateConfig.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/config/StateConfig.java @@ -66,9 +66,6 @@ * time a signed state reference count is changed, and logged if a signed state * reference count bug is detected. * @param emergencyStateFileName The name of the file that contains the emergency state. - * @param signedStateFreq hash and sign a state every signedStateFreq rounds. 1 means that a state will be - * signed every round, 2 means every other round, and so on. If the value is 0 or - * less, no states will be signed * @param deleteInvalidStateFiles At startup time, if a state can not be deserialized without errors, should we * delete that state from disk and try another? If true then states that can't be * parsed are deleted. If false then a node will crash if it can't parse a state @@ -98,7 +95,6 @@ public record StateConfig( @ConfigProperty(defaultValue = "false") boolean stateHistoryEnabled, @ConfigProperty(defaultValue = "false") boolean debugStackTracesEnabled, @ConfigProperty(defaultValue = "emergencyRecovery.yaml") String emergencyStateFileName, - @ConfigProperty(defaultValue = "1") int signedStateFreq, @ConfigProperty(defaultValue = "false") boolean deleteInvalidStateFiles, @ConfigProperty(defaultValue = "true") boolean validateInitialState) { diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/error/CatastrophicIssTrigger.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/error/CatastrophicIssTrigger.java deleted file mode 100644 index f11504cce5e2..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/error/CatastrophicIssTrigger.java +++ /dev/null @@ -1,44 +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.error; - -import com.swirlds.common.crypto.Hash; -import com.swirlds.platform.dispatch.types.TriggerTwo; - -/** - *

    - * Trigger warning. - *

    - * - *

    - * Sends dispatches for catastrophic ISS events. - *

    - */ -@FunctionalInterface -public interface CatastrophicIssTrigger extends TriggerTwo { - - /** - * Signal that there has been a catastrophic ISS. - * - * @param round - * the round of the ISS - * @param selfStateHash - * the hash computed by this node - */ - @Override - void dispatch(Long round, Hash selfStateHash); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/error/SelfIssTrigger.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/error/SelfIssTrigger.java deleted file mode 100644 index 0991c1bf8789..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/error/SelfIssTrigger.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.error; - -import com.swirlds.common.crypto.Hash; -import com.swirlds.platform.dispatch.types.TriggerThree; - -/** - * Sends dispatches for self ISS events. - */ -@FunctionalInterface -public interface SelfIssTrigger extends TriggerThree { - - /** - * Signal that there has been a self ISS. - * - * @param round - * the round of the ISS - * @param selfStateHash - * the incorrect hash computed by this node - * @param consensusHash - * the correct hash computed by the network - */ - @Override - void dispatch(Long round, Hash selfStateHash, Hash consensusHash); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/flow/DiskStateLoadedTrigger.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/flow/DiskStateLoadedTrigger.java deleted file mode 100644 index ac1052f6fd2e..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/flow/DiskStateLoadedTrigger.java +++ /dev/null @@ -1,38 +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.flow; - -import com.swirlds.common.crypto.Hash; -import com.swirlds.platform.dispatch.types.TriggerTwo; - -/** - * Dispatches when a state has been loaded from disk. - */ -@FunctionalInterface -public interface DiskStateLoadedTrigger extends TriggerTwo { - - /** - * Signal that a state has been loaded from disk. - * - * @param round - * the round of the state that was loaded - * @param stateHash - * the hash of the state that was loaded - */ - @Override - void dispatch(Long round, Hash stateHash); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/flow/ReconnectStateLoadedTrigger.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/flow/ReconnectStateLoadedTrigger.java deleted file mode 100644 index 178309e4c7b3..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/flow/ReconnectStateLoadedTrigger.java +++ /dev/null @@ -1,38 +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.flow; - -import com.swirlds.common.crypto.Hash; -import com.swirlds.platform.dispatch.types.TriggerTwo; - -/** - * Dispatches when a state has been loaded via reconnect. - */ -@FunctionalInterface -public interface ReconnectStateLoadedTrigger extends TriggerTwo { - - /** - * Signal that a valid state has been received via reconnect. - * - * @param round - * the round of the state that was obtained - * @param stateHash - * the hash of the state that was obtained - */ - @Override - void dispatch(Long round, Hash stateHash); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/flow/StateHashValidityTrigger.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/flow/StateHashValidityTrigger.java deleted file mode 100644 index 54574af28a7f..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/flow/StateHashValidityTrigger.java +++ /dev/null @@ -1,50 +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.flow; - -import com.swirlds.common.crypto.Hash; -import com.swirlds.common.platform.NodeId; -import com.swirlds.platform.dispatch.types.TriggerFour; -import edu.umd.cs.findbugs.annotations.NonNull; - -/** - * Sends dispatches when the validity of a node's reported state hash has been determined. - * Not sent for rounds that have a catastrophic ISS (these rounds don't have a consensus hash, - * so there is no such thing as a valid hash for that round). - */ -@FunctionalInterface -public interface StateHashValidityTrigger extends TriggerFour { - - /** - * Signal that the validity of a reported state hash can be determined - * - * @param round - * the round number - * @param nodeId - * the ID of the node that submitted the hash - * @param nodeHash - * the hash computed by the node - * @param consensusHash - * the consensus hash computed by the network - */ - @Override - void dispatch( - @NonNull final Long round, - @NonNull final NodeId nodeId, - @NonNull final Hash nodeHash, - @NonNull final Hash consensusHash); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/transaction/PostConsensusStateSignatureTrigger.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/transaction/PostConsensusStateSignatureTrigger.java deleted file mode 100644 index 20fe4bb01df8..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/transaction/PostConsensusStateSignatureTrigger.java +++ /dev/null @@ -1,44 +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.transaction; - -import com.swirlds.common.crypto.Hash; -import com.swirlds.common.crypto.Signature; -import com.swirlds.common.platform.NodeId; -import com.swirlds.platform.dispatch.types.TriggerFour; - -/** - * Sends dispatches for state signatures that have reached consensus. - */ -@FunctionalInterface -public interface PostConsensusStateSignatureTrigger extends TriggerFour { - - /** - * Signal that a tate signature transaction has reached consensus and is ready to be handled. - * - * @param round - * the round that was signed - * @param signerId - * the ID of the signer - * @param hash - * the hash that was signed - * @param signature - * the signature on the hash - */ - @Override - void dispatch(Long round, NodeId signerId, Hash hash, Signature signature); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/transaction/PreConsensusStateSignatureTrigger.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/transaction/PreConsensusStateSignatureTrigger.java deleted file mode 100644 index 33f0dc68d42b..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/dispatch/triggers/transaction/PreConsensusStateSignatureTrigger.java +++ /dev/null @@ -1,44 +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.transaction; - -import com.swirlds.common.crypto.Hash; -import com.swirlds.common.crypto.Signature; -import com.swirlds.common.platform.NodeId; -import com.swirlds.platform.dispatch.types.TriggerFour; - -/** - * Sends dispatches for pre-consensus state signatures. - */ -@FunctionalInterface -public interface PreConsensusStateSignatureTrigger extends TriggerFour { - - /** - * Signal that a pre-consensus state signature is ready to be handled. - * - * @param round - * the round that was signed - * @param signerId - * the ID of the signer - * @param hash - * the hash that was signed - * @param signature - * the signature on the hash - */ - @Override - void dispatch(Long round, NodeId signerId, Hash hash, Signature signature); -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/ConsensusQueue.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/ConsensusQueue.java deleted file mode 100644 index 5d3310c15bc2..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/ConsensusQueue.java +++ /dev/null @@ -1,378 +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.eventhandling; - -import com.swirlds.platform.internal.ConsensusRound; -import com.swirlds.platform.metrics.ConsensusHandlingMetrics; -import java.util.Collection; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * A specialized implementation of {@link BlockingQueue} for the consensus queue (q2) that stores consensus rounds - * to be handled. Despite the queue storing rounds, the capacity to store additional rounds is determined by the total - * number of events in those rounds. - *

    - * The implementation does not support multiple threads adding elements or multiple threads removing elements, but it - * does support a single thread that adds elements that is different from the thread removing elements. - */ -public class ConsensusQueue implements BlockingQueue { - - /** The total number of events in all the rounds in the queue at any given time. */ - private final AtomicInteger eventsInQueue = new AtomicInteger(0); - - /** The statistics instance to update */ - private final ConsensusHandlingMetrics consensusHandlingMetrics; - - /** The maximum number of events allowed in rounds in the queue */ - private final int eventCapacity; - - /** The queue that holds the rounds */ - private final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); - - /** - * Creates a new instance with no items in the queue. - * - * @param consensusHandlingMetrics - * the stats object to record stats on - * @param eventCapacity - * the maximum number of events allowed in all the rounds in the queue, unless there is a single round in the - * queue with more than this many events - */ - ConsensusQueue(final ConsensusHandlingMetrics consensusHandlingMetrics, final int eventCapacity) { - super(); - this.consensusHandlingMetrics = consensusHandlingMetrics; - this.eventCapacity = eventCapacity; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean add(final ConsensusRound consensusRound) { - throw new UnsupportedOperationException("use put() instead"); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean offer(final ConsensusRound consensusRound) { - if (queueHasRoom(consensusRound)) { - final boolean ans = queue.offer(consensusRound); - if (ans) { - incrementEventsInQueue(consensusRound); - } - return ans; - } - return false; - } - - /** - * {@inheritDoc} - */ - @Override - public ConsensusRound remove() { - final ConsensusRound round = queue.remove(); - if (round == null) { - throw new NoSuchElementException("queue is empty"); - } - decrementEventsInQueue(round); - return round; - } - - /** - * {@inheritDoc} - */ - @Override - public ConsensusRound poll() { - final ConsensusRound round = queue.poll(); - if (round != null) { - decrementEventsInQueue(round); - } - return round; - } - - /** - * {@inheritDoc} - */ - @Override - public ConsensusRound element() { - final ConsensusRound round = queue.element(); - if (round == null) { - throw new NoSuchElementException("queue is empty"); - } - return round; - } - - /** - * Retrieves, but does not remove, the head of this queue, - * or returns {@code null} if this queue is empty. - * - * @return the head of this queue, or {@code null} if this queue is empty - */ - @Override - public ConsensusRound peek() { - return queue.peek(); - } - - /** - * Adds the round to the queue (q2) if there is room according to the event capacity. If there is no room, wait - * until there is. - * - * @param consensusRound - * the round to add to the queue - * @throws InterruptedException - * if this thread is interrupted while waiting or adding to the queue - */ - @Override - public synchronized void put(final ConsensusRound consensusRound) throws InterruptedException { - consensusHandlingMetrics.recordEventsPerRound(consensusRound.getNumEvents()); - while (!queueHasRoom(consensusRound)) { - this.wait(); - } - - queue.put(consensusRound); - incrementEventsInQueue(consensusRound); - } - - private void incrementEventsInQueue(final ConsensusRound consensusRound) { - eventsInQueue.updateAndGet(currValue -> currValue + consensusRound.getNumEvents()); - } - - /** - * {@inheritDoc} - */ - @Override - public synchronized boolean offer(final ConsensusRound consensusRound, final long timeout, final TimeUnit unit) - throws InterruptedException { - consensusHandlingMetrics.recordEventsPerRound(consensusRound.getNumEvents()); - final boolean ans; - long millisWaited = 0; - final long maxMillisToWait = unit.toMillis(timeout); - while (!queueHasRoom(consensusRound) && millisWaited <= maxMillisToWait) { - millisWaited++; - this.wait(1); - } - - if (!queueHasRoom(consensusRound)) { - return false; - } - - ans = queue.offer(consensusRound); - if (ans) { - incrementEventsInQueue(consensusRound); - } - return ans; - } - - /** - * {@inheritDoc} - */ - @Override - public ConsensusRound take() throws InterruptedException { - final ConsensusRound round = queue.take(); - decrementEventsInQueue(round); - return round; - } - - /** - * {@inheritDoc} - */ - @Override - public ConsensusRound poll(final long timeout, final TimeUnit unit) throws InterruptedException { - final ConsensusRound round = queue.poll(timeout, unit); - if (round != null) { - decrementEventsInQueue(round); - } - return round; - } - - /** - * Returns the number of additional elements that this queue can ideally - * (in the absence of memory or resource constraints) accept without - * blocking, or {@code Integer.MAX_VALUE} if there is no intrinsic - * limit. - * - *

    Note that you cannot always tell if an attempt to insert - * an element will succeed by inspecting {@code remainingCapacity} - * because it may be the case that another thread is about to - * insert or remove an element. - * - * @return the remaining capacity - */ - @Override - public int remainingCapacity() { - return eventCapacity - eventsInQueue.get(); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean remove(final Object o) { - throw new UnsupportedOperationException("remove() is not supported by ConsensusQueue"); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean containsAll(final Collection c) { - return queue.containsAll(c); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean contains(final Object o) { - return queue.contains(o); - } - - /** - * {@inheritDoc} - */ - @Override - public Iterator iterator() { - return queue.iterator(); - } - - /** - * {@inheritDoc} - */ - @Override - public Object[] toArray() { - return queue.toArray(); - } - - /** - * {@inheritDoc} - */ - @Override - public T[] toArray(final T[] a) { - return queue.toArray(a); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean addAll(final Collection c) { - throw new UnsupportedOperationException("addAll() is not supported by ConsensusQueue"); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean removeAll(final Collection c) { - throw new UnsupportedOperationException("removeAll() is not supported by ConsensusQueue"); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean retainAll(final Collection c) { - throw new UnsupportedOperationException("retainAll() is not supported by ConsensusQueue"); - } - - /** - * {@inheritDoc} - */ - @Override - public void clear() { - queue.clear(); - eventsInQueue.set(0); - } - - /** - * Returns the total number of events in all the rounds in this queue. - */ - @Override - public int size() { - return eventsInQueue.get(); - } - - /** - * Returns {@code true} if this collection contains no elements. - * - * @return {@code true} if this collection contains no elements - */ - @Override - public boolean isEmpty() { - return queue.isEmpty(); - } - - /** - * {@inheritDoc} - */ - @Override - public int drainTo(final Collection c) { - final int numDrained = queue.drainTo(c); - for (final Object round : c) { - decrementEventsInQueue((ConsensusRound) round); - } - return numDrained; - } - - /** - * {@inheritDoc} - */ - @Override - public int drainTo(final Collection c, final int maxElements) { - final int numDrained = queue.drainTo(c, maxElements); - for (final Object round : c) { - decrementEventsInQueue((ConsensusRound) round); - } - return numDrained; - } - - /** - * Determines if the {@code consensusRound} can be added to the queue based on the event capacity. If the queue is - * currently empty, return true regardless of the number of events in the round. If the queue is not empty, check if - * adding the round will exceed the maximum number of events. - * - * @param consensusRound - * the round to add - * @return true if the round can be added - */ - private boolean queueHasRoom(final ConsensusRound consensusRound) { - final int numEvents = consensusRound.getNumEvents(); - if (queue.isEmpty()) { - return true; - } - return eventsInQueue.get() + numEvents <= eventCapacity; - } - - /** - * Decrement the number of events in the queue by the number of events in {@code consensusRound}. The handler of - * items in the queue is responsible for calling this method. - * - * @param consensusRound - * the round just removed from the queue - */ - private synchronized void decrementEventsInQueue(final ConsensusRound consensusRound) { - eventsInQueue.updateAndGet(currValue -> currValue - consensusRound.getNumEvents()); - this.notifyAll(); - } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/ConsensusRoundHandler.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/ConsensusRoundHandler.java index 05706dc65ce9..a9ec2c52e8c0 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/ConsensusRoundHandler.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/ConsensusRoundHandler.java @@ -17,51 +17,40 @@ package com.swirlds.platform.eventhandling; import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; -import static com.swirlds.logging.legacy.LogMarker.RECONNECT; -import static com.swirlds.logging.legacy.LogMarker.STARTUP; -import static com.swirlds.platform.SwirldsPlatform.PLATFORM_THREAD_POOL_NAME; +import static com.swirlds.platform.eventhandling.ConsensusRoundHandlerPhase.CREATING_SIGNED_STATE; +import static com.swirlds.platform.eventhandling.ConsensusRoundHandlerPhase.GETTING_STATE_TO_SIGN; +import static com.swirlds.platform.eventhandling.ConsensusRoundHandlerPhase.HANDLING_CONSENSUS_ROUND; +import static com.swirlds.platform.eventhandling.ConsensusRoundHandlerPhase.IDLE; +import static com.swirlds.platform.eventhandling.ConsensusRoundHandlerPhase.MARKING_ROUND_COMPLETE; +import static com.swirlds.platform.eventhandling.ConsensusRoundHandlerPhase.SETTING_EVENT_CONSENSUS_DATA; +import static com.swirlds.platform.eventhandling.ConsensusRoundHandlerPhase.UPDATING_PLATFORM_STATE; +import static com.swirlds.platform.eventhandling.ConsensusRoundHandlerPhase.UPDATING_PLATFORM_STATE_RUNNING_HASH; +import static com.swirlds.platform.eventhandling.ConsensusRoundHandlerPhase.WAITING_FOR_EVENT_DURABILITY; +import static com.swirlds.platform.eventhandling.ConsensusRoundHandlerPhase.WAITING_FOR_PREHANDLE; import com.swirlds.base.function.CheckedConsumer; -import com.swirlds.base.state.Startable; import com.swirlds.common.context.PlatformContext; -import com.swirlds.common.crypto.CryptographyHolder; import com.swirlds.common.crypto.DigestType; import com.swirlds.common.crypto.Hash; import com.swirlds.common.crypto.ImmutableHash; import com.swirlds.common.crypto.RunningHash; import com.swirlds.common.metrics.RunningAverageMetric; -import com.swirlds.common.platform.NodeId; -import com.swirlds.common.stream.EventStreamManager; -import com.swirlds.common.threading.framework.QueueThread; -import com.swirlds.common.threading.framework.Stoppable; -import com.swirlds.common.threading.framework.config.QueueThreadConfiguration; -import com.swirlds.common.threading.framework.config.QueueThreadMetricsConfiguration; -import com.swirlds.common.threading.manager.ThreadManager; -import com.swirlds.common.utility.Clearable; -import com.swirlds.metrics.api.FloatFormats; +import com.swirlds.common.stream.RunningEventHashUpdate; import com.swirlds.metrics.api.Metrics; -import com.swirlds.platform.config.StateConfig; -import com.swirlds.platform.config.ThreadConfig; import com.swirlds.platform.consensus.ConsensusConfig; import com.swirlds.platform.event.GossipEvent; import com.swirlds.platform.internal.ConsensusRound; import com.swirlds.platform.internal.EventImpl; -import com.swirlds.platform.metrics.ConsensusHandlingMetrics; -import com.swirlds.platform.observers.ConsensusRoundObserver; +import com.swirlds.platform.metrics.RoundHandlingMetrics; import com.swirlds.platform.state.PlatformState; import com.swirlds.platform.state.State; import com.swirlds.platform.state.SwirldStateManager; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.state.signed.SignedState; -import com.swirlds.platform.stats.AverageAndMax; -import com.swirlds.platform.stats.AverageStat; -import com.swirlds.platform.stats.CycleTimingStat; -import com.swirlds.platform.system.PlatformStatNames; import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.status.StatusActionSubmitter; import com.swirlds.platform.system.status.actions.FreezePeriodEnteredAction; import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.Arrays; import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.function.Consumer; @@ -69,50 +58,37 @@ import org.apache.logging.log4j.Logger; /** - * Created by a Platform to manage the flow of consensus events to SwirldState (1 instance or 3 depending on the - * SwirldState implemented). It contains a thread queue that contains a queue of consensus events (q2) and a - * SwirldStateManager which applies those events to the state. It also creates signed states at the appropriate times. + * Applies transactions from consensus rounds to the state */ -public class ConsensusRoundHandler implements ConsensusRoundObserver, Clearable, Startable { +public class ConsensusRoundHandler { - /** - * use this for all logging, as controlled by the optional data/log4j2.xml file - */ private static final Logger logger = LogManager.getLogger(ConsensusRoundHandler.class); /** - * The name of the thread that handles consensus events + * The name of the thread that handles transactions. For the sake of the app, to allow logging. */ - public static final String THREAD_CONS_NAME = "thread_cons"; + public static final String TRANSACTION_HANDLING_THREAD_NAME = ""; /** * The class responsible for all interactions with the swirld state */ private final SwirldStateManager swirldStateManager; - private final ConsensusHandlingMetrics consensusHandlingMetrics; - - /** - * The queue thread that stores consensus rounds and feeds them to this class for handling. - */ - private final QueueThread queueThread; - - /** - * Stores consensus events in the event stream. - */ - private final EventStreamManager eventStreamManager; + private final RoundHandlingMetrics handlerMetrics; /** - * indicates whether a state was saved in the current freeze period. we are only saving the first state in the - * freeze period. this variable is only used by threadCons so there is no synchronization needed + * Whether a round in a freeze period has been received. This may never be reset to false after it is set to true. */ - private boolean savedStateInFreeze = false; + private boolean freezeRoundReceived = false; /** * a RunningHash object which calculates running hash of all consensus events so far with their transactions handled - * by stateCons + *

    + * Future work: this will need to be changed to a proper null hash when this component is updated to handle empty + * rounds, since a hash of all 0s isn't valid when deserializing a state. In the current system, this hash is + * always overwritten before being put into the state, so it doesn't matter that it starts as all 0s. */ - private RunningHash eventsConsRunningHash = + private RunningHash consensusEventsRunningHash = new RunningHash(new ImmutableHash(new byte[DigestType.SHA_384.digestLength()])); /** @@ -125,8 +101,6 @@ public class ConsensusRoundHandler implements ConsensusRoundObserver, Clearable, */ private final StatusActionSubmitter statusActionSubmitter; - private boolean addedFirstRoundInFreeze = false; - private final SoftwareVersion softwareVersion; private final Consumer roundAppliedToStateConsumer; @@ -134,7 +108,7 @@ public class ConsensusRoundHandler implements ConsensusRoundObserver, Clearable, /** * A method that blocks until an event becomes durable. */ - final CheckedConsumer waitForEventDurability; + private final CheckedConsumer waitForEventDurability; /** * The number of non-ancient rounds. @@ -149,28 +123,20 @@ public class ConsensusRoundHandler implements ConsensusRoundObserver, Clearable, .withUnit("count"); /** - * Instantiate, but don't start any threads yet. The Platform should first instantiate the - * {@link ConsensusRoundHandler}. Then the Platform should call start to start the queue thread. + * Constructor * - * @param platformContext contains various platform utilities - * @param threadManager responsible for creating and managing threads - * @param selfId the id of this node - * @param swirldStateManager the swirld state manager to send events to - * @param consensusHandlingMetrics statistics updated by {@link ConsensusRoundHandler} - * @param eventStreamManager the event stream manager to send consensus events to - * @param stateHashSignQueue the queue thread that handles hashing and collecting signatures of new - * self-signed states - * @param waitForEventDurability a method that blocks until an event becomes durable. - * @param statusActionSubmitter enables submitting of platform status actions - * @param softwareVersion the current version of the software + * @param platformContext contains various platform utilities + * @param swirldStateManager the swirld state manager to send events to + * @param stateHashSignQueue the queue thread that handles hashing and collecting signatures of new + * self-signed states + * @param waitForEventDurability a method that blocks until an event becomes durable + * @param statusActionSubmitter enables submitting of platform status actions + * @param roundAppliedToStateConsumer informs the consensus hash manager that a round has been applied to state + * @param softwareVersion the current version of the software */ public ConsensusRoundHandler( @NonNull final PlatformContext platformContext, - @NonNull final ThreadManager threadManager, - @NonNull final NodeId selfId, @NonNull final SwirldStateManager swirldStateManager, - @NonNull final ConsensusHandlingMetrics consensusHandlingMetrics, - @NonNull final EventStreamManager eventStreamManager, @NonNull final BlockingQueue stateHashSignQueue, @NonNull final CheckedConsumer waitForEventDurability, @NonNull final StatusActionSubmitter statusActionSubmitter, @@ -178,273 +144,109 @@ public ConsensusRoundHandler( @NonNull final SoftwareVersion softwareVersion) { this.platformContext = Objects.requireNonNull(platformContext); - this.roundAppliedToStateConsumer = roundAppliedToStateConsumer; - Objects.requireNonNull(selfId, "selfId must not be null"); - this.swirldStateManager = swirldStateManager; - this.consensusHandlingMetrics = consensusHandlingMetrics; - this.eventStreamManager = eventStreamManager; - this.stateHashSignQueue = stateHashSignQueue; + this.swirldStateManager = Objects.requireNonNull(swirldStateManager); + this.stateHashSignQueue = Objects.requireNonNull(stateHashSignQueue); + this.waitForEventDurability = Objects.requireNonNull(waitForEventDurability); this.statusActionSubmitter = Objects.requireNonNull(statusActionSubmitter); + this.roundAppliedToStateConsumer = Objects.requireNonNull(roundAppliedToStateConsumer); + this.softwareVersion = Objects.requireNonNull(softwareVersion); - this.softwareVersion = softwareVersion; - - final EventConfig eventConfig = platformContext.getConfiguration().getConfigData(EventConfig.class); - final ConsensusQueue queue = new ConsensusQueue(consensusHandlingMetrics, eventConfig.maxEventQueueForCons()); - final ThreadConfig threadConfig = platformContext.getConfiguration().getConfigData(ThreadConfig.class); - - queueThread = new QueueThreadConfiguration(threadManager) - .setNodeId(selfId) - .setHandler(this::applyConsensusRoundToState) - .setComponent(PLATFORM_THREAD_POOL_NAME) - .setThreadName(THREAD_CONS_NAME) - .setStopBehavior(Stoppable.StopBehavior.BLOCKING) - .setLogAfterPauseDuration(threadConfig.logStackTracePauseDuration()) - .setMetricsConfiguration( - new QueueThreadMetricsConfiguration(platformContext.getMetrics()).enableBusyTimeMetric()) - .setQueue(queue) - .build(); - - roundsNonAncient = platformContext + this.roundsNonAncient = platformContext .getConfiguration() .getConfigData(ConsensusConfig.class) .roundsNonAncient(); + this.handlerMetrics = new RoundHandlingMetrics(platformContext); - this.waitForEventDurability = waitForEventDurability; - - final AverageAndMax avgQ2ConsEvents = new AverageAndMax( - platformContext.getMetrics(), - Metrics.INTERNAL_CATEGORY, - PlatformStatNames.CONSENSUS_QUEUE_SIZE, - "average number of events in the consensus queue (q2) waiting to be handled", - FloatFormats.FORMAT_10_3, - AverageStat.WEIGHT_VOLATILE); + // Future work: This metric should be moved to a suitable component once the stateHashSignQueue is migrated + // to the framework final RunningAverageMetric avgStateToHashSignDepth = platformContext.getMetrics().getOrCreate(AVG_STATE_TO_HASH_SIGN_DEPTH_CONFIG); - platformContext.getMetrics().addUpdater(() -> { - avgQ2ConsEvents.update(queueThread.size()); - avgStateToHashSignDepth.update(getStateToHashSignSize()); - }); - } - - /** - * Starts the queue thread. - */ - @Override - public void start() { - queueThread.start(); - } - - /** - * Stops the queue thread. For unit testing purposes only. - */ - public void stop() { - queueThread.stop(); + platformContext.getMetrics().addUpdater(() -> avgStateToHashSignDepth.update(stateHashSignQueue.size())); } /** - * Blocks until the handling thread has handled all available work and is no longer busy. May block indefinitely if - * more work is continually added to the queue. + * Update the consensus event running hash + *

    + * Future work: in the current system, it isn't actually necessary to update this running hash when a new state + * is loaded. The running hash will be overwritten anyway by the first round that contains events, before the + * initial hash is ever set in the state. This method is being called anyway, though, since it will be a necessary + * workflow in the future to support handling of empty rounds by this component. * - * @throws InterruptedException if interrupted while waiting - */ - public void waitUntilNotBusy() throws InterruptedException { - queueThread.waitUntilNotBusy(); - } - - @Override - public void clear() { - logger.info(RECONNECT.getMarker(), "consensus handler: clearing queue thread"); - queueThread.clear(); - - logger.info(RECONNECT.getMarker(), "consensus handler: clearing stateHashSignQueue queue"); - clearStateHashSignQueueThread(); - - // clear running Hash info - eventsConsRunningHash = new RunningHash(new ImmutableHash(new byte[DigestType.SHA_384.digestLength()])); - - logger.info(RECONNECT.getMarker(), "consensus handler: ready for reconnect"); - } - - /** - * Clears and releases any signed states in the {@code stateHashSignQueueThread} queue. + * @param runningHashUpdate the update to the running hash */ - private void clearStateHashSignQueueThread() { - ReservedSignedState signedState = stateHashSignQueue.poll(); - while (signedState != null) { - signedState.close(); - signedState = stateHashSignQueue.poll(); - } + public void updateRunningHash(@NonNull final RunningEventHashUpdate runningHashUpdate) { + consensusEventsRunningHash = new RunningHash(runningHashUpdate.runningEventHash()); } /** - * Loads data from a SignedState, this is used on startup to load events and the running hash that have been - * previously saved on disk + * Applies the transactions in the consensus round to the state * - * @param signedState the state to load data from - * @param isReconnect if it is true, the reservedSignedState is loaded at reconnect; if it is false, the - * reservedSignedState is loaded at startup - */ - public void loadDataFromSignedState(final SignedState signedState, final boolean isReconnect) { - // set initialHash of the RunningHash to be the hash loaded from signed state - eventsConsRunningHash = new RunningHash(signedState.getHashEventsCons()); - - logger.info( - STARTUP.getMarker(), - "consensus event handler minGenFamous after startup: {}", - () -> Arrays.toString(signedState.getMinGenInfo().toArray())); - - // get startRunningHash from reservedSignedState - final Hash initialHash = new Hash(signedState.getHashEventsCons()); - eventStreamManager.setInitialHash(initialHash); - - logger.info(STARTUP.getMarker(), "initialHash after startup {}", () -> initialHash); - eventStreamManager.setStartWriteAtCompleteWindow(isReconnect); - } - - /** - * {@inheritDoc} + * @param consensusRound the consensus round to apply */ - @Override - public void consensusRound(final ConsensusRound consensusRound) { - if (consensusRound == null || consensusRound.getConsensusEvents().isEmpty()) { - // we ignore rounds with no events for now + public void handleConsensusRound(@NonNull final ConsensusRound consensusRound) { + // consensus rounds with no events are ignored + if (consensusRound.isEmpty()) { + // Future work: the long term goal is for empty rounds to not be ignored here. For now, the way that the + // running hash of consensus events is calculated by the EventStreamManager prevents that from being + // possible. return; } - if (!addedFirstRoundInFreeze && isRoundInFreezePeriod(consensusRound)) { - addedFirstRoundInFreeze = true; - statusActionSubmitter.submitStatusAction(new FreezePeriodEnteredAction(consensusRound.getRoundNum())); - } - - addConsensusRound(consensusRound); - } - - private boolean isRoundInFreezePeriod(final ConsensusRound round) { - if (round.isEmpty()) { - // there are no events in this round - return false; - } - return swirldStateManager.isInFreezePeriod(round.getConsensusTimestamp()); - } - - /** - * Add a consensus event to the queue (q2) for handling. - * - * @param consensusRound the consensus round to add - */ - private void addConsensusRound(final ConsensusRound consensusRound) { - try { - // adds this consensus event to eventStreamHelper, - // which will put it into a queue for calculating runningHash, and a queue for event streaming when enabled - eventStreamManager.addEvents(consensusRound.getConsensusEvents()); - // this may block until the queue isn't full - queueThread.put(consensusRound); - } catch (final InterruptedException e) { - logger.error(EXCEPTION.getMarker(), "addEvent interrupted"); - Thread.currentThread().interrupt(); - } - } - - /** - * Adds the consensus events in the round to the eventsAndGenerations queue and feeds their transactions to the - * consensus state object (which is a SwirldState representing the effect of all consensus transactions so far). It - * also creates the signed state if Settings.signedStateFreq > 0 and this is a round for which it should be done. - * - * @throws InterruptedException if this thread was interrupted while adding a signed state to the signed state - * queue - */ - private void applyConsensusRoundToState(final ConsensusRound round) throws InterruptedException { - // If there has already been a saved state created in a freeze period, do not apply any more rounds to the - // state until the node shuts down and comes back up (which resets this variable to false). - if (savedStateInFreeze) { + // Once there is a saved state created in a freeze period, we will never apply any more rounds to the state. + if (freezeRoundReceived) { return; } - final CycleTimingStat consensusTimingStat = consensusHandlingMetrics.getConsCycleStat(); - consensusTimingStat.startCycle(); - - waitForEventDurability.accept(round.getKeystoneEvent().getBaseEvent()); - - consensusTimingStat.setTimePoint(1); - - propagateConsensusData(round); - updatePlatformState(round); - - if (round.getEventCount() > 0) { - consensusHandlingMetrics.recordConsensusTime(round.getConsensusTimestamp()); + if (swirldStateManager.isInFreezePeriod(consensusRound.getConsensusTimestamp())) { + statusActionSubmitter.submitStatusAction(new FreezePeriodEnteredAction(consensusRound.getRoundNum())); + freezeRoundReceived = true; } - swirldStateManager.handleConsensusRound(round); - consensusTimingStat.setTimePoint(2); + handlerMetrics.recordEventsPerRound(consensusRound.getNumEvents()); + handlerMetrics.recordConsensusTime(consensusRound.getConsensusTimestamp()); - roundAppliedToStateConsumer.accept(round.getRoundNum()); - - consensusTimingStat.setTimePoint(3); - - consensusTimingStat.setTimePoint(4); + try { + handlerMetrics.setPhase(WAITING_FOR_EVENT_DURABILITY); + waitForEventDurability.accept(consensusRound.getKeystoneEvent().getBaseEvent()); - EventImpl lastEvent = null; - for (final EventImpl event : round.getConsensusEvents()) { - lastEvent = event; - if (event.getHash() == null) { - CryptographyHolder.get().digestSync(event); + handlerMetrics.setPhase(SETTING_EVENT_CONSENSUS_DATA); + for (final EventImpl event : consensusRound.getConsensusEvents()) { + event.consensusReached(); } - } - // update the running hash object - // if there are no events, the running hash does not change - if (lastEvent != null) { - eventsConsRunningHash = lastEvent.getRunningHash(); - } + handlerMetrics.setPhase(UPDATING_PLATFORM_STATE); + // it is important to update the platform state before handling the consensus round, since the platform + // state is passed into the application handle method, and should contain the data for the current round + updatePlatformState(consensusRound); - // time point 3 to the end is misleading on its own because it is recorded even when no signed state is created - // . For an accurate stat on how much time it takes to create a signed state, refer to - // newSignedStateCycleTiming in Statistics - consensusTimingStat.setTimePoint(5); - updateRunningEventHash(); + handlerMetrics.setPhase(WAITING_FOR_PREHANDLE); + consensusRound.forEach(event -> ((EventImpl) event).getBaseEvent().awaitPrehandleCompletion()); - consensusTimingStat.setTimePoint(6); + handlerMetrics.setPhase(HANDLING_CONSENSUS_ROUND); + swirldStateManager.handleConsensusRound(consensusRound); - // If the round should be signed (because the settings say so), create the signed state - if (timeToSignState(round.getRoundNum())) { - if (isRoundInFreezePeriod(round)) { - // We are saving the first state in the freeze period. - // This should never be set to false once it is true. It is reset by restarting the node - savedStateInFreeze = true; + handlerMetrics.setPhase(MARKING_ROUND_COMPLETE); + // this calls into the ConsensusHashManager + roundAppliedToStateConsumer.accept(consensusRound.getRoundNum()); + + handlerMetrics.setPhase(UPDATING_PLATFORM_STATE_RUNNING_HASH); + updatePlatformStateRunningHash(consensusRound); - // Let the swirld state manager know we are about to write the saved state for the freeze period - swirldStateManager.savedStateInFreezePeriod(); - } createSignedState(); + } catch (final InterruptedException e) { + logger.error(EXCEPTION.getMarker(), "handleConsensusRound interrupted"); + Thread.currentThread().interrupt(); + } finally { + handlerMetrics.setPhase(IDLE); } - consensusTimingStat.stopCycle(); } /** - * Propagates consensus data from every event to every transaction. + * Populate the {@link com.swirlds.platform.state.PlatformState PlatformState} with all needed data for this round. * - * @param round the round of events to propagate data in + * @param round the consensus round */ - private void propagateConsensusData(final ConsensusRound round) { - for (final EventImpl event : round.getConsensusEvents()) { - event.consensusReached(); - } - } - - private boolean timeToSignState(final long roundNum) { - final StateConfig stateConfig = platformContext.getConfiguration().getConfigData(StateConfig.class); - return stateConfig.signedStateFreq() > 0 // and we are signing states - - // the first round should be signed and every Nth should be signed, where N is signedStateFreq - && (roundNum == 1 || roundNum % stateConfig.signedStateFreq() == 0); - } - - /** - * Populate the {@link com.swirlds.platform.state.PlatformState PlatformState} with all of its needed data for this - * round, with the exception of the running event hash. Wait until transactions are handled before updating this. - * This makes it less likely that we will have to wait for the hash to be computed. - */ - private void updatePlatformState(final ConsensusRound round) { + private void updatePlatformState(@NonNull final ConsensusRound round) { final PlatformState platformState = swirldStateManager.getConsensusState().getPlatformState(); @@ -456,43 +258,46 @@ private void updatePlatformState(final ConsensusRound round) { } /** - * Update the running event hash in the platform state. + * Update the running hash of the consensus events in the platform state + *

    + * This method is called after the consensus round has been applied to the state, to minimize the chance that + * we have to wait for the running hash to finish being calculated. + * + * @param round the consensus round + * @throws InterruptedException if this thread is interrupted */ - private void updateRunningEventHash() throws InterruptedException { + private void updatePlatformStateRunningHash(@NonNull final ConsensusRound round) throws InterruptedException { final PlatformState platformState = swirldStateManager.getConsensusState().getPlatformState(); - final Hash runningHash = eventsConsRunningHash.getFutureHash().getAndRethrow(); + + // Update the running hash object. If there are no events, the running hash does not change. + // Future work: this is a redundant check, since empty rounds are currently ignored entirely. The check is here + // anyway, for when that changes in the future. + if (!round.isEmpty()) { + consensusEventsRunningHash = round.getConsensusEvents().getLast().getRunningHash(); + } + + final Hash runningHash = consensusEventsRunningHash.getFutureHash().getAndRethrow(); platformState.setRunningEventHash(runningHash); } + /** + * Create a signed state + * + * @throws InterruptedException if this thread is interrupted + */ private void createSignedState() throws InterruptedException { - final CycleTimingStat ssTimingStat = consensusHandlingMetrics.getNewSignedStateCycleStat(); - ssTimingStat.startCycle(); + if (freezeRoundReceived) { + // Let the swirld state manager know we are about to write the saved state for the freeze period + swirldStateManager.savedStateInFreezePeriod(); + } - // create a new signed state, sign it, and send out a new transaction with the signature - // the signed state keeps a copy that never changes. + handlerMetrics.setPhase(GETTING_STATE_TO_SIGN); final State immutableStateCons = swirldStateManager.getStateForSigning(); - ssTimingStat.setTimePoint(1); - + handlerMetrics.setPhase(CREATING_SIGNED_STATE); final SignedState signedState = new SignedState( - platformContext, immutableStateCons, "ConsensusHandler.createSignedState()", savedStateInFreeze); - - ssTimingStat.setTimePoint(2); - - stateHashSignQueue.put(signedState.reserve("ConsensusHandler.createSignedState()")); - - ssTimingStat.stopCycle(); - } - - public int getRoundsInQueue() { - return queueThread.size(); - } - - /** - * {@inheritDoc} - */ - public int getStateToHashSignSize() { - return stateHashSignQueue.size(); + platformContext, immutableStateCons, "ConsensusRoundHandler.createSignedState()", freezeRoundReceived); + stateHashSignQueue.put(signedState.reserve("ConsensusRoundHandler.createSignedState()")); } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/ConsensusRoundHandlerPhase.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/ConsensusRoundHandlerPhase.java new file mode 100644 index 000000000000..6532be696646 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/ConsensusRoundHandlerPhase.java @@ -0,0 +1,64 @@ +/* + * 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.eventhandling; + +/** + * The phase of the consensus round handling process. + */ +public enum ConsensusRoundHandlerPhase { + /** + * Nothing is happening. + */ + IDLE, + /** + * The handler is waiting for the keystone event of the round to become durable before applying the contained + * transactions to the state. + */ + WAITING_FOR_EVENT_DURABILITY, + /** + * The consensus fields of the events in a round are being populated. + */ + SETTING_EVENT_CONSENSUS_DATA, + /** + * The round handler is waiting for transaction prehandling to complete. + */ + WAITING_FOR_PREHANDLE, + /** + * The transactions in the round are being applied to the state. + */ + HANDLING_CONSENSUS_ROUND, + /** + * The platform state is being updated with results from the round. + */ + UPDATING_PLATFORM_STATE, + /** + * The platform state is being updated with the running hash of the round. + */ + UPDATING_PLATFORM_STATE_RUNNING_HASH, + /** + * The consensus hash manager is being informed that the round has been handled. + */ + MARKING_ROUND_COMPLETE, + /** + * The handler is getting the state to sign. + */ + GETTING_STATE_TO_SIGN, + /** + * The handler is creating a new signed state instance. + */ + CREATING_SIGNED_STATE +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/ConsensusHandlingMetrics.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/ConsensusHandlingMetrics.java deleted file mode 100644 index 1e90cc49e4b9..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/ConsensusHandlingMetrics.java +++ /dev/null @@ -1,161 +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.metrics; - -import static com.swirlds.metrics.api.FloatFormats.FORMAT_8_1; -import static com.swirlds.metrics.api.Metrics.INTERNAL_CATEGORY; - -import com.swirlds.base.time.Time; -import com.swirlds.base.utility.Pair; -import com.swirlds.metrics.api.LongGauge; -import com.swirlds.metrics.api.Metrics; -import com.swirlds.platform.eventhandling.ConsensusRoundHandler; -import com.swirlds.platform.internal.ConsensusRound; -import com.swirlds.platform.stats.AverageAndMax; -import com.swirlds.platform.stats.AverageStat; -import com.swirlds.platform.stats.CycleTimingStat; -import com.swirlds.platform.stats.cycle.CycleDefinition; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.Objects; - -/** - * Provides access to statistics relevant to {@link ConsensusRoundHandler} - */ -public class ConsensusHandlingMetrics { - private final CycleTimingStat consensusCycleTiming; - - private final CycleTimingStat newSignedStateCycleTiming; - - private final AverageAndMax avgEventsPerRound; - - private static final LongGauge.Config consensusTimeConfig = new LongGauge.Config(INTERNAL_CATEGORY, "consensusTime") - .withDescription("The consensus timestamp of the round currently being handled.") - .withUnit("milliseconds"); - private final LongGauge consensusTime; - - private static final LongGauge.Config consensusTimeDeviationConfig = new LongGauge.Config( - INTERNAL_CATEGORY, "consensusTimeDeviation") - .withDescription("The difference between the consensus time of " - + "the round currently being handled and this node's wall clock time. " - + "Positive values mean that this node's clock is behind the consensus time, " - + "negative values mean that it's ahead.") - .withUnit("milliseconds"); - private final LongGauge consensusTimeDeviation; - - private final Time time; - - /** - * Constructor of {@code ConsensusHandlingMetrics} - * - * @param metrics - * a reference to the metrics-system - * @param time provides wall clock time - * @throws NullPointerException in case {@code metrics} parameter is {@code null} - */ - public ConsensusHandlingMetrics(final Metrics metrics, final Time time) { - Objects.requireNonNull(metrics, "metrics must not be null"); - this.time = time; - - consensusCycleTiming = new CycleTimingStat( - metrics, - ChronoUnit.MILLIS, - new CycleDefinition( - INTERNAL_CATEGORY, - "consRound", - List.of( - Pair.of( - "keystoneFlushMillis_per_round", - "average time to flush a round's keystone event to disk"), - Pair.of( - "dataPropMillis_per_round", - "average time to propagate consensus data to transactions"), - Pair.of("handleMillis_per_round", "average time to handle a consensus round"), - Pair.of( - "storeMillis_per_round", - "average time to add consensus round events to signed state storage"), - Pair.of( - "hashMillis_per_round", - "average time spent hashing the consensus round events"), - Pair.of("buildStateMillis", "average time spent building a signed state"), - Pair.of( - "forSigCleanMillis", - "average time spent expiring signed state storage events")))); - newSignedStateCycleTiming = new CycleTimingStat( - metrics, - ChronoUnit.MICROS, - new CycleDefinition( - INTERNAL_CATEGORY, - "newSS", - List.of( - Pair.of("getStateMicros", "average time to get the state to sign"), - Pair.of( - "newSSInstanceMicros", - "average time spent creating the new signed state instance"), - Pair.of( - "queueAdmitMicros", - "average time spent admitting the signed state to the signing queue")))); - avgEventsPerRound = new AverageAndMax( - metrics, - INTERNAL_CATEGORY, - "events_per_round", - "average number of events in a consensus round", - FORMAT_8_1, - AverageStat.WEIGHT_VOLATILE); - - consensusTime = metrics.getOrCreate(consensusTimeConfig); - consensusTimeDeviation = metrics.getOrCreate(consensusTimeDeviationConfig); - } - - /** - * @return the cycle timing stat that keeps track of how much time is spent in various parts of {@link - * ConsensusRoundHandler#consensusRound(ConsensusRound)} - */ - public CycleTimingStat getConsCycleStat() { - return consensusCycleTiming; - } - - /** - * @return the cycle timing stat that keeps track of how much time is spent creating a new signed state in {@link - * ConsensusRoundHandler#consensusRound(ConsensusRound)} - */ - public CycleTimingStat getNewSignedStateCycleStat() { - return newSignedStateCycleTiming; - } - - /** - * Records the number of events in a round. - * - * @param numEvents - * the number of events in the round - */ - public void recordEventsPerRound(final int numEvents) { - avgEventsPerRound.update(numEvents); - } - - /** - * Records the consensus time. - * - * @param consensusTime - * the consensus time of the last transaction in the round that is currently being handled - */ - public void recordConsensusTime(final Instant consensusTime) { - this.consensusTime.set(consensusTime.toEpochMilli()); - consensusTimeDeviation.set(consensusTime.toEpochMilli() - time.now().toEpochMilli()); - } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/IssMetrics.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/IssMetrics.java index 1e7c8e99292b..311d7f16efef 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/IssMetrics.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/IssMetrics.java @@ -21,9 +21,6 @@ import com.swirlds.metrics.api.IntegerGauge; import com.swirlds.metrics.api.LongGauge; import com.swirlds.metrics.api.Metrics; -import com.swirlds.platform.dispatch.Observer; -import com.swirlds.platform.dispatch.triggers.error.CatastrophicIssTrigger; -import com.swirlds.platform.dispatch.triggers.flow.StateHashValidityTrigger; import com.swirlds.platform.system.address.Address; import com.swirlds.platform.system.address.AddressBook; import edu.umd.cs.findbugs.annotations.NonNull; @@ -153,7 +150,6 @@ public long getIssWeight() { * @param consensusHash * the consensus hash computed by the network */ - @Observer(StateHashValidityTrigger.class) public void stateHashValidityObserver( @NonNull final Long round, @NonNull final NodeId nodeId, @@ -199,12 +195,8 @@ public void stateHashValidityObserver( * * @param round * the round of the ISS - * @param selfStateHash - * the hash computed by this node */ - @Observer(CatastrophicIssTrigger.class) - public void catastrophicIssObserver(final Long round, final Hash selfStateHash) { - + public void catastrophicIssObserver(final long round) { if (round <= highestRound) { // Don't report old data return; diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/RoundHandlingMetrics.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/RoundHandlingMetrics.java new file mode 100644 index 000000000000..7df0c6feabe0 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/RoundHandlingMetrics.java @@ -0,0 +1,111 @@ +/* + * 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.metrics; + +import static com.swirlds.metrics.api.Metrics.INTERNAL_CATEGORY; +import static com.swirlds.platform.eventhandling.ConsensusRoundHandlerPhase.IDLE; + +import com.swirlds.base.time.Time; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.metrics.extensions.PhaseTimer; +import com.swirlds.common.metrics.extensions.PhaseTimerBuilder; +import com.swirlds.metrics.api.LongGauge; +import com.swirlds.metrics.api.Metrics; +import com.swirlds.platform.eventhandling.ConsensusRoundHandler; +import com.swirlds.platform.eventhandling.ConsensusRoundHandlerPhase; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Instant; +import java.util.Objects; + +/** + * Provides access to statistics relevant to {@link ConsensusRoundHandler} + */ +public class RoundHandlingMetrics { + private static final LongGauge.Config consensusTimeConfig = new LongGauge.Config(INTERNAL_CATEGORY, "consensusTime") + .withDescription("The consensus timestamp of the round currently being handled.") + .withUnit("milliseconds"); + private final LongGauge consensusTime; + + private static final LongGauge.Config consensusTimeDeviationConfig = new LongGauge.Config( + INTERNAL_CATEGORY, "consensusTimeDeviation") + .withDescription("The difference between the consensus time of the round currently being handled and this" + + " node's wall clock time. Positive values mean that this node's clock is behind the consensus" + + "time, negative values mean that it's ahead.") + .withUnit("milliseconds"); + private final LongGauge consensusTimeDeviation; + + private static final LongGauge.Config eventsPerRoundConfig = new LongGauge.Config( + INTERNAL_CATEGORY, "eventsPerRound") + .withDescription("The number of events per round") + .withUnit("count"); + private final LongGauge eventsPerRound; + + private final PhaseTimer roundHandlerPhase; + + private final Time time; + + /** + * Constructor + * + * @param platformContext the platform context + */ + public RoundHandlingMetrics(@NonNull final PlatformContext platformContext) { + this.time = platformContext.getTime(); + + final Metrics metrics = platformContext.getMetrics(); + + consensusTime = metrics.getOrCreate(consensusTimeConfig); + consensusTimeDeviation = metrics.getOrCreate(consensusTimeDeviationConfig); + eventsPerRound = metrics.getOrCreate(eventsPerRoundConfig); + + this.roundHandlerPhase = new PhaseTimerBuilder<>( + platformContext, time, "platform", ConsensusRoundHandlerPhase.class) + .enableFractionalMetrics() + .setInitialPhase(IDLE) + .setMetricsNamePrefix("consensus") + .build(); + } + + /** + * Records the number of events in a round. + * + * @param eventCount the number of events in the round + */ + public void recordEventsPerRound(final int eventCount) { + eventsPerRound.set(eventCount); + } + + /** + * Records the consensus time. + * + * @param consensusTime the consensus time of the last transaction in the round that is currently being handled + */ + public void recordConsensusTime(@NonNull final Instant consensusTime) { + this.consensusTime.set(consensusTime.toEpochMilli()); + consensusTimeDeviation.set(consensusTime.toEpochMilli() - time.now().toEpochMilli()); + } + + /** + * Activate a new phase of the consensus round handler. + * + * @param phase the new phase + */ + public void setPhase(@NonNull final ConsensusRoundHandlerPhase phase) { + Objects.requireNonNull(phase); + roundHandlerPhase.activatePhase(phase); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/ConsensusHashManager.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssDetector.java similarity index 66% rename from platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/ConsensusHashManager.java rename to platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssDetector.java index bfb2df78c9c2..defca9b1c9a6 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/ConsensusHashManager.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssDetector.java @@ -19,8 +19,9 @@ 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.base.time.Time; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.crypto.Hash; import com.swirlds.common.platform.NodeId; @@ -28,25 +29,24 @@ import com.swirlds.common.sequence.map.SequenceMap; 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.config.StateConfig; import com.swirlds.platform.consensus.ConsensusConfig; -import com.swirlds.platform.dispatch.DispatchBuilder; -import com.swirlds.platform.dispatch.Observer; -import com.swirlds.platform.dispatch.triggers.error.CatastrophicIssTrigger; -import com.swirlds.platform.dispatch.triggers.error.SelfIssTrigger; -import com.swirlds.platform.dispatch.triggers.flow.DiskStateLoadedTrigger; -import com.swirlds.platform.dispatch.triggers.flow.ReconnectStateLoadedTrigger; -import com.swirlds.platform.dispatch.triggers.flow.StateHashValidityTrigger; -import com.swirlds.platform.dispatch.triggers.flow.StateHashedTrigger; +import com.swirlds.platform.metrics.IssMetrics; import com.swirlds.platform.state.iss.internal.ConsensusHashFinder; import com.swirlds.platform.state.iss.internal.HashValidityStatus; import com.swirlds.platform.state.iss.internal.RoundHashValidator; +import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.system.SoftwareVersion; 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 edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -54,9 +54,9 @@ /** * Keeps track of the state hashes reported by all network nodes. Responsible for detecting ISS events. */ -public class ConsensusHashManager { +public class IssDetector { - private static final Logger logger = LogManager.getLogger(ConsensusHashManager.class); + private static final Logger logger = LogManager.getLogger(IssDetector.class); private final SequenceMap roundData; @@ -105,16 +105,13 @@ public class ConsensusHashManager { * A round that should not be validated. Set to {@link #DO_NOT_IGNORE_ROUNDS} if all rounds should be validated. */ private final long ignoredRound; - - private final SelfIssTrigger selfIssDispatcher; - private final CatastrophicIssTrigger catastrophicIssDispatcher; - private final StateHashValidityTrigger stateHashValidityDispatcher; + /** ISS related metrics */ + private final IssMetrics issMetrics; /** * Create an object that tracks reported hashes and detects ISS events. * - * @param time provides the current wall clock time - * @param dispatchBuilder responsible for building dispatchers + * @param platformContext the platform context * @param addressBook the address book for the network * @param currentEpochHash the current epoch hash * @param currentSoftwareVersion the current software version @@ -123,37 +120,27 @@ public class ConsensusHashManager { * @param ignoredRound a round that should not be validated. Set to {@link #DO_NOT_IGNORE_ROUNDS} if * all rounds should be validated. */ - public ConsensusHashManager( + public IssDetector( @NonNull final PlatformContext platformContext, - final Time time, - final DispatchBuilder dispatchBuilder, - final AddressBook addressBook, - final Hash currentEpochHash, + @NonNull final AddressBook addressBook, + @Nullable final Hash currentEpochHash, @NonNull final SoftwareVersion currentSoftwareVersion, final boolean ignorePreconsensusSignatures, final long ignoredRound) { - - Objects.requireNonNull(currentSoftwareVersion); + Objects.requireNonNull(platformContext); final ConsensusConfig consensusConfig = platformContext.getConfiguration().getConfigData(ConsensusConfig.class); final StateConfig stateConfig = platformContext.getConfiguration().getConfigData(StateConfig.class); final Duration timeBetweenIssLogs = Duration.ofSeconds(stateConfig.secondsBetweenIssLogs()); - lackingSignaturesRateLimiter = new RateLimiter(time, timeBetweenIssLogs); - selfIssRateLimiter = new RateLimiter(time, timeBetweenIssLogs); - catastrophicIssRateLimiter = new RateLimiter(time, timeBetweenIssLogs); - - this.selfIssDispatcher = dispatchBuilder.getDispatcher( - ConsensusHashManager.class, SelfIssTrigger.class, "self ISS detected")::dispatch; - this.catastrophicIssDispatcher = dispatchBuilder.getDispatcher( - ConsensusHashManager.class, CatastrophicIssTrigger.class, "really bad ISS detected")::dispatch; - this.stateHashValidityDispatcher = dispatchBuilder.getDispatcher( - ConsensusHashManager.class, StateHashValidityTrigger.class, "round ISS status known")::dispatch; - - this.addressBook = addressBook; + lackingSignaturesRateLimiter = new RateLimiter(platformContext.getTime(), timeBetweenIssLogs); + selfIssRateLimiter = new RateLimiter(platformContext.getTime(), timeBetweenIssLogs); + catastrophicIssRateLimiter = new RateLimiter(platformContext.getTime(), timeBetweenIssLogs); + + this.addressBook = Objects.requireNonNull(addressBook); this.currentEpochHash = currentEpochHash; - this.currentSoftwareVersion = currentSoftwareVersion; + this.currentSoftwareVersion = Objects.requireNonNull(currentSoftwareVersion); this.roundData = new ConcurrentSequenceMap<>( -consensusConfig.roundsNonAncient(), consensusConfig.roundsNonAncient(), x -> x); @@ -167,21 +154,23 @@ public ConsensusHashManager( if (ignoredRound != DO_NOT_IGNORE_ROUNDS) { logger.warn(STARTUP.getMarker(), "No ISS detection will be performed for round {}", ignoredRound); } + this.issMetrics = new IssMetrics(platformContext.getMetrics(), addressBook); } /** * This method is called once all preconsensus events have been replayed. */ - public void signalEndOfPreconsensusReplay() { + public void signalEndOfPreconsensusReplay(@Nullable final Object ignored) { replayingPreconsensusStream = false; } /** - * Observes when a round has been completed. + * Called when a round has been completed. * * @param round the round that was just completed + * @return a list of ISS notifications, or null if no ISS occurred */ - public void roundCompleted(final long round) { + public @Nullable List roundCompleted(final long round) { if (round <= previousRound) { throw new IllegalArgumentException( "previous round was " + previousRound + ", can't decrease round to " + round); @@ -189,32 +178,35 @@ public void roundCompleted(final long round) { if (round == ignoredRound) { // This round is intentionally ignored. - return; + return null; } final long oldestRoundToValidate = round - roundData.getSequenceNumberCapacity() + 1; + final List removedRounds = new ArrayList<>(); if (round != 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. roundData.shiftWindow(oldestRoundToValidate); } else { - roundData.shiftWindow(oldestRoundToValidate, this::handleRemovedRound); + roundData.shiftWindow(oldestRoundToValidate, (k, v) -> removedRounds.add(v)); } final long roundWeight = addressBook.getTotalWeight(); previousRound = round; - roundData.put(round, new RoundHashValidator(stateHashValidityDispatcher, round, roundWeight)); + + roundData.put(round, new RoundHashValidator(round, roundWeight, issMetrics)); + return listOrNull(removedRounds.stream().map(this::handleRemovedRound).toList()); } /** * Handle a round that has become old enough that we want to stop tracking data on it. * - * @param round the round that is old * @param roundHashValidator the hash validator for the round + * @return an ISS notification, or null if no ISS occurred */ - private void handleRemovedRound(final long round, final RoundHashValidator roundHashValidator) { + private @Nullable IssNotification handleRemovedRound(@NonNull final RoundHashValidator roundHashValidator) { final boolean justDecided = roundHashValidator.outOfTime(); final StringBuilder sb = new StringBuilder(); @@ -226,6 +218,7 @@ private void handleRemovedRound(final long round, final RoundHashValidator round if (status == HashValidityStatus.CATASTROPHIC_ISS || status == HashValidityStatus.CATASTROPHIC_LACK_OF_DATA) { handleCatastrophic(roundHashValidator); + return new IssNotification(roundHashValidator.getRound(), IssType.CATASTROPHIC_ISS); } else if (status == HashValidityStatus.LACK_OF_DATA) { handleLackOfData(roundHashValidator); } else { @@ -233,6 +226,19 @@ private void handleRemovedRound(final long round, final RoundHashValidator round "Unexpected hash validation status " + status + ", should have decided prior to now"); } } + return null; + } + + /** + * Handle postconsensus state signatures. + * + * @param transactions the signature transactions to handle + * @return a list of ISS notifications, or null if no ISS occurred + */ + public @Nullable List handlePostconsensusSignatures( + @NonNull final List> transactions) { + return listOrNull( + transactions.stream().map(this::handlePostconsensusSignature).toList()); } /** @@ -245,46 +251,43 @@ private void handleRemovedRound(final long round, final RoundHashValidator round * signature transaction observed here (post consensus) will be for a round in the past. *

    * - * @param signerId the ID of the node that signed the state - * @param signatureTransaction the signature transaction - * @param eventVersion the version of the event that contains the transaction + * @param transaction the transaction to handle + * @return an ISS notification, or null if no ISS occurred */ - public void handlePostconsensusSignatureTransaction( - @NonNull final NodeId signerId, - @NonNull final StateSignatureTransaction signatureTransaction, - @Nullable final SoftwareVersion eventVersion) { - - Objects.requireNonNull(signerId); - Objects.requireNonNull(signatureTransaction); + private @Nullable IssNotification handlePostconsensusSignature( + @NonNull final ScopedSystemTransaction transaction) { + final NodeId signerId = transaction.submitterId(); + final StateSignatureTransaction signatureTransaction = transaction.transaction(); + final SoftwareVersion eventVersion = transaction.softwareVersion(); if (eventVersion == null) { // Illegal event version, ignore. - return; + return null; } if (ignorePreconsensusSignatures && replayingPreconsensusStream) { // We are still replaying preconsensus events and we are configured to ignore signatures during replay - return; + return null; } if (currentSoftwareVersion.compareTo(eventVersion) != 0) { // this is a signature from a different software version, ignore it - return; + return null; } if (!Objects.equals(signatureTransaction.getEpochHash(), currentEpochHash)) { // this is a signature from a different epoch, ignore it - return; + return null; } if (!addressBook.contains(signerId)) { // we don't care about nodes not in the address book - return; + return null; } if (signatureTransaction.getRound() == ignoredRound) { // This round is intentionally ignored. - return; + return null; } final long nodeWeight = addressBook.getAddress(signerId).getWeight(); @@ -293,27 +296,41 @@ public void handlePostconsensusSignatureTransaction( if (roundValidator == null) { // We are being asked to validate a signature from the far future or far past, or a round that has already // been decided. - return; + return null; } final boolean decided = roundValidator.reportHashFromNetwork(signerId, nodeWeight, signatureTransaction.getStateHash()); if (decided) { - checkValidity(roundValidator); + return checkValidity(roundValidator); } + return null; } /** - * Observe when this node finishes hashing a state. + * 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. * * @param round the round of the state * @param hash the hash of the state + * @return an ISS notification, or null if no ISS occurred */ - @Observer(value = StateHashedTrigger.class, comment = "check hash derived by this node") - public void stateHashedObserver(final Long round, final Hash hash) { + private @Nullable IssNotification newStateHashed(final long round, @NonNull final Hash hash) { if (round == ignoredRound) { // This round is intentionally ignored. - return; + return null; } final RoundHashValidator roundHashValidator = roundData.get(round); @@ -324,45 +341,60 @@ public void stateHashedObserver(final Long round, final Hash hash) { final boolean decided = roundHashValidator.reportSelfHash(hash); if (decided) { - checkValidity(roundHashValidator); + return checkValidity(roundHashValidator); } + return null; } /** - * Observe when an overriding state is obtained, i.e. via reconnect or state loading. + * Called when an overriding state is obtained, i.e. via reconnect or state loading. * - * @param round the round of the state that was obtained - * @param stateHash the hash of the state that was obtained + * @param state the state that was loaded + * @return a list of ISS notifications, or null if no ISS occurred */ - @Observer( - value = {DiskStateLoadedTrigger.class, ReconnectStateLoadedTrigger.class}, - comment = "ingest completed state") - public void overridingStateObserver(final Long round, final Hash stateHash) { - roundCompleted(round); - stateHashedObserver(round, stateHash); + public @Nullable List overridingState(@NonNull final ReservedSignedState state) { + try (state) { + final long round = state.get().getRound(); + final Hash stateHash = state.get().getState().getHash(); + // 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)); + } } /** * Called once the validity has been decided. Take action based on the validity status. * * @param roundValidator the validator for the round + * @return an ISS notification, or null if no ISS occurred */ - private void checkValidity(final RoundHashValidator roundValidator) { + private @Nullable IssNotification checkValidity(@NonNull final RoundHashValidator roundValidator) { final long round = roundValidator.getRound(); - switch (roundValidator.getStatus()) { + return switch (roundValidator.getStatus()) { case VALID -> { - // :) + if (roundValidator.hasDisagreement()) { + yield new IssNotification(round, IssType.OTHER_ISS); + } + yield null; + } + case SELF_ISS -> { + handleSelfIss(roundValidator); + yield new IssNotification(round, IssType.SELF_ISS); + } + case CATASTROPHIC_ISS -> { + handleCatastrophic(roundValidator); + yield new IssNotification(round, IssType.CATASTROPHIC_ISS); } - case SELF_ISS -> handleSelfIss(roundValidator); - case CATASTROPHIC_ISS -> handleCatastrophic(roundValidator); case UNDECIDED -> throw new IllegalStateException( "status is undecided, but method reported a decision, round = " + round); case LACK_OF_DATA -> throw new IllegalStateException( "a decision that we lack data should only be possible once time runs out, round = " + round); default -> throw new IllegalStateException( "unhandled case " + roundValidator.getStatus() + ", round = " + round); - } + }; } /** @@ -370,7 +402,7 @@ private void checkValidity(final RoundHashValidator roundValidator) { * * @param roundHashValidator the validator responsible for validating the round with a self ISS */ - private void handleSelfIss(final RoundHashValidator roundHashValidator) { + private void handleSelfIss(@NonNull final RoundHashValidator roundHashValidator) { final long round = roundHashValidator.getRound(); final Hash selfHash = roundHashValidator.getSelfStateHash(); final Hash consensusHash = roundHashValidator.getConsensusHash(); @@ -390,8 +422,6 @@ private void handleSelfIss(final RoundHashValidator roundHashValidator) { EXCEPTION.getMarker(), new IssPayload(sb.toString(), round, selfHash.toMnemonic(), consensusHash.toMnemonic(), false)); } - - selfIssDispatcher.dispatch(round, selfHash, consensusHash); } /** @@ -399,7 +429,7 @@ private void handleSelfIss(final RoundHashValidator roundHashValidator) { * * @param roundHashValidator information about the round, including the signatures that were gathered */ - private void handleCatastrophic(final RoundHashValidator roundHashValidator) { + private void handleCatastrophic(@NonNull final RoundHashValidator roundHashValidator) { final long round = roundHashValidator.getRound(); final ConsensusHashFinder hashFinder = roundHashValidator.getHashFinder(); @@ -418,8 +448,6 @@ private void handleCatastrophic(final RoundHashValidator roundHashValidator) { logger.fatal(EXCEPTION.getMarker(), new IssPayload(sb.toString(), round, selfHash.toMnemonic(), "", true)); } - - catastrophicIssDispatcher.dispatch(round, selfHash); } /** @@ -427,7 +455,7 @@ private void handleCatastrophic(final RoundHashValidator roundHashValidator) { * * @param roundHashValidator information about the round */ - private void handleLackOfData(final RoundHashValidator roundHashValidator) { + private void handleLackOfData(@NonNull final RoundHashValidator roundHashValidator) { final long skipCount = lackingSignaturesRateLimiter.getDeniedRequests(); if (!lackingSignaturesRateLimiter.requestAndTrigger()) { return; @@ -453,7 +481,7 @@ private void handleLackOfData(final RoundHashValidator roundHashValidator) { /** * Write the number of times a log has been skipped. */ - private static void writeSkippedLogCount(final StringBuilder sb, final long skipCount) { + private static void writeSkippedLogCount(@NonNull final StringBuilder sb, final long skipCount) { if (skipCount > 0) { sb.append("This condition has been triggered ") .append(skipCount) @@ -462,4 +490,24 @@ private static void writeSkippedLogCount(final StringBuilder sb, final long skip .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/state/iss/IssHandler.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/IssHandler.java index e9bb00ff9924..b28a8d8ab543 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 @@ -16,23 +16,13 @@ package com.swirlds.platform.state.iss; -import com.swirlds.common.crypto.Hash; import com.swirlds.common.merkle.utility.SerializableLong; -import com.swirlds.common.platform.NodeId; import com.swirlds.common.scratchpad.Scratchpad; import com.swirlds.platform.components.common.output.FatalErrorConsumer; -import com.swirlds.platform.components.state.output.IssConsumer; import com.swirlds.platform.config.StateConfig; -import com.swirlds.platform.dispatch.Observer; import com.swirlds.platform.dispatch.triggers.control.HaltRequestedConsumer; -import com.swirlds.platform.dispatch.triggers.error.CatastrophicIssTrigger; -import com.swirlds.platform.dispatch.triggers.error.SelfIssTrigger; -import com.swirlds.platform.dispatch.triggers.flow.StateHashValidityTrigger; import com.swirlds.platform.system.SystemExitCode; import com.swirlds.platform.system.state.notifications.IssNotification; -import com.swirlds.platform.system.status.StatusActionSubmitter; -import com.swirlds.platform.system.status.actions.CatastrophicFailureAction; -import com.swirlds.platform.system.status.actions.PlatformStatusAction; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Objects; @@ -42,86 +32,52 @@ public class IssHandler { private final StateConfig stateConfig; private final HaltRequestedConsumer haltRequestedConsumer; - private final IssConsumer issConsumer; private final FatalErrorConsumer fatalErrorConsumer; private final Scratchpad issScratchpad; private boolean halted; - private final NodeId selfId; - - /** - * Allows for submitting - * {@link PlatformStatusAction PlatformStatusActions} - */ - private final StatusActionSubmitter statusActionSubmitter; - /** * Create an object responsible for handling ISS events. * * @param stateConfig settings for the state - * @param selfId the self ID of this node - * @param statusActionSubmitter the object to use to submit status actions * @param haltRequestedConsumer consumer to invoke when a system halt is desired * @param fatalErrorConsumer consumer to invoke if a fatal error occurs - * @param issConsumer consumer to invoke if an ISS is detected * @param issScratchpad scratchpad for ISS data, is persistent across restarts */ public IssHandler( @NonNull final StateConfig stateConfig, - @NonNull final NodeId selfId, - @NonNull final StatusActionSubmitter statusActionSubmitter, @NonNull final HaltRequestedConsumer haltRequestedConsumer, @NonNull final FatalErrorConsumer fatalErrorConsumer, - @NonNull final IssConsumer issConsumer, @NonNull final Scratchpad issScratchpad) { - - this.issConsumer = Objects.requireNonNull(issConsumer, "issConsumer must not be null"); this.haltRequestedConsumer = Objects.requireNonNull(haltRequestedConsumer, "haltRequestedConsumer must not be null"); this.fatalErrorConsumer = Objects.requireNonNull(fatalErrorConsumer, "fatalErrorConsumer must not be null"); - this.stateConfig = Objects.requireNonNull(stateConfig, "stateConfig must not be null"); - - this.selfId = Objects.requireNonNull(selfId, "selfId must not be null"); - - this.statusActionSubmitter = Objects.requireNonNull(statusActionSubmitter); - this.issScratchpad = Objects.requireNonNull(issScratchpad); } /** - * This method is called whenever any node is observed in disagreement with the consensus hash. + * This method is called whenever an ISS event is observed. * - * @param round the round of the ISS - * @param nodeId the ID of the node that had an ISS - * @param nodeHash the incorrect hash computed by the node - * @param consensusHash the correct hash computed by the network + * @param issNotification the notification of the ISS event */ - @Observer(StateHashValidityTrigger.class) - public void stateHashValidityObserver( - @NonNull final Long round, - @NonNull final NodeId nodeId, - @NonNull final Hash nodeHash, - @NonNull final Hash consensusHash) { - - if (consensusHash.equals(nodeHash)) { - // no need to take action when the hash is valid - return; - } - - if (Objects.equals(nodeId, selfId)) { - // let the logic in selfIssObserver handle self ISS events - return; + public void issObserved(@NonNull final IssNotification issNotification) { + switch (issNotification.getIssType()) { + case SELF_ISS -> selfIssObserver(issNotification.getRound()); + case OTHER_ISS -> otherIss(); + case CATASTROPHIC_ISS -> catastrophicIssObserver(issNotification.getRound()); } + } + /** + * This method is called whenever any node is observed in disagreement with the consensus hash. + */ + private void otherIss() { if (halted) { // don't take any action once halted return; } - - issConsumer.iss(round, IssNotification.IssType.OTHER_ISS, nodeId); - if (stateConfig.haltOnAnyIss()) { haltRequestedConsumer.haltRequested("other node observed with ISS"); halted = true; @@ -153,11 +109,8 @@ private void updateIssRoundInScratchpad(final long issRound) { * This method is called when there is a self ISS. * * @param round the round of the ISS - * @param ignored1 the incorrect hash computed by this node - * @param ignored2 the correct hash computed by the network */ - @Observer(SelfIssTrigger.class) - public void selfIssObserver(@NonNull final Long round, @NonNull final Hash ignored1, @NonNull final Hash ignored2) { + private void selfIssObserver(@NonNull final Long round) { if (halted) { // don't take any action once halted @@ -165,9 +118,6 @@ public void selfIssObserver(@NonNull final Long round, @NonNull final Hash ignor } updateIssRoundInScratchpad(round); - statusActionSubmitter.submitStatusAction(new CatastrophicFailureAction()); - - issConsumer.iss(round, IssNotification.IssType.SELF_ISS, selfId); if (stateConfig.haltOnAnyIss()) { haltRequestedConsumer.haltRequested("self ISS observed"); @@ -233,11 +183,9 @@ public void selfIssObserver(@NonNull final Long round, @NonNull final Hash ignor /** * This method is called when there is a catastrophic ISS. * - * @param round the round of the ISS - * @param ignored the hash computed by this node + * @param round the round of the ISS */ - @Observer(CatastrophicIssTrigger.class) - public void catastrophicIssObserver(@NonNull final Long round, @NonNull final Hash ignored) { + private void catastrophicIssObserver(@NonNull final Long round) { if (halted) { // don't take any action once halted @@ -245,9 +193,6 @@ public void catastrophicIssObserver(@NonNull final Long round, @NonNull final Ha } updateIssRoundInScratchpad(round); - statusActionSubmitter.submitStatusAction(new CatastrophicFailureAction()); - - issConsumer.iss(round, IssNotification.IssType.CATASTROPHIC_ISS, null); if (stateConfig.haltOnAnyIss() || stateConfig.haltOnCatastrophicIss()) { haltRequestedConsumer.haltRequested("catastrophic ISS observed"); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/internal/ConsensusHashFinder.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/internal/ConsensusHashFinder.java index 860a299ff64b..1f5978cff430 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/internal/ConsensusHashFinder.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/internal/ConsensusHashFinder.java @@ -20,7 +20,7 @@ import com.swirlds.common.crypto.Hash; import com.swirlds.common.platform.NodeId; -import com.swirlds.platform.dispatch.triggers.flow.StateHashValidityTrigger; +import com.swirlds.platform.metrics.IssMetrics; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; import java.util.Collections; @@ -56,6 +56,8 @@ public class ConsensusHashFinder { */ private final long round; + private final IssMetrics issMetrics; + /** * The total weight of nodes that have reported their hash for this round. */ @@ -76,24 +78,17 @@ public class ConsensusHashFinder { */ private Hash consensusHash; - private final StateHashValidityTrigger stateHashValidityDispatcher; - /** - * Create a new object for tracking agreement on the hash of a particular round. + * Create a new object for tracking agreement on the hash of a particular round against the consensus hash * - * @param stateHashValidityDispatcher - * a dispatch method that should be called whenever a reported hash can be verified - * against the consensus hash - * @param round - * the current round - * @param totalWeight - * the total weight contained within the network for this round + * @param round the current round + * @param totalWeight the total weight contained within the network for this round + * @param issMetrics iss related metrics */ - public ConsensusHashFinder( - final StateHashValidityTrigger stateHashValidityDispatcher, final long round, final long totalWeight) { - this.stateHashValidityDispatcher = stateHashValidityDispatcher; + public ConsensusHashFinder(final long round, final long totalWeight, @NonNull final IssMetrics issMetrics) { this.round = round; this.totalWeight = totalWeight; + this.issMetrics = Objects.requireNonNull(issMetrics); } /** @@ -156,7 +151,6 @@ public void addHash(@NonNull final NodeId nodeId, final long nodeWeight, @NonNul if (status != ConsensusHashStatus.UNDECIDED) { sendHashValidityDispatch(nodeId, stateHash); - // Once we know the status, the status never changes. return; } @@ -168,11 +162,12 @@ public void addHash(@NonNull final NodeId nodeId, final long nodeWeight, @NonNul status = ConsensusHashStatus.DECIDED; sendHashValidityDispatchForAllNodes(); } else { - long remainingWeight = totalWeight - hashReportedWeight; + final long remainingWeight = totalWeight - hashReportedWeight; if (!MAJORITY.isSatisfiedBy(largestPartition.getTotalWeight() + remainingWeight, totalWeight)) { // There exists no partition with quorum, and there will never exist a partition with a quorum. // Heaven help us. status = ConsensusHashStatus.CATASTROPHIC_ISS; + issMetrics.catastrophicIssObserver(round); } } } @@ -189,7 +184,7 @@ private void sendHashValidityDispatch(@NonNull final NodeId nodeId, @NonNull fin Objects.requireNonNull(nodeId, "nodeId must not be null"); Objects.requireNonNull(stateHash, "stateHash must not be null"); if (consensusHash != null) { - stateHashValidityDispatcher.dispatch(round, nodeId, stateHash, consensusHash); + issMetrics.stateHashValidityObserver(round, nodeId, stateHash, consensusHash); } } @@ -211,6 +206,13 @@ public Map getPartitionMap() { return partitionMap; } + /** + * @return true if there is any disagreement between nodes on the hash for this round + */ + public boolean hasDisagreement() { + return partitionMap.size() > 1; + } + /** * Get the total weight in the network. */ diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/internal/RoundHashValidator.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/internal/RoundHashValidator.java index cfeb0eae5d98..92efbd98e30e 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/internal/RoundHashValidator.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/internal/RoundHashValidator.java @@ -21,7 +21,7 @@ import com.swirlds.common.crypto.Hash; import com.swirlds.common.platform.NodeId; -import com.swirlds.platform.dispatch.triggers.flow.StateHashValidityTrigger; +import com.swirlds.platform.metrics.IssMetrics; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Objects; import org.apache.logging.log4j.LogManager; @@ -71,18 +71,13 @@ public class RoundHashValidator { /** * Create an object that validates this node's hash for a round. * - * @param stateHashValidityDispatcher - * a dispatch method should be called when there is a hash disagreement - * @param round - * the round number - * @param roundWeight - * the total weight for this round + * @param round the round number + * @param roundWeight the total weight for this round + * @param issMetrics iss related metrics */ - public RoundHashValidator( - final StateHashValidityTrigger stateHashValidityDispatcher, final long round, final long roundWeight) { - + public RoundHashValidator(final long round, final long roundWeight, @NonNull final IssMetrics issMetrics) { this.round = round; - hashFinder = new ConsensusHashFinder(stateHashValidityDispatcher, round, roundWeight); + hashFinder = new ConsensusHashFinder(round, roundWeight, Objects.requireNonNull(issMetrics)); } /** @@ -95,14 +90,14 @@ public long getRound() { /** * Get the hash that this node computed for the round if it is known, or null if it is not known. */ - public synchronized Hash getSelfStateHash() { + public Hash getSelfStateHash() { return selfStateHash; } /** * Get the consensus hash if it is known, or null if it is unknown. */ - public synchronized Hash getConsensusHash() { + public Hash getConsensusHash() { if (hashFinder.getStatus() == ConsensusHashStatus.DECIDED) { return hashFinder.getConsensusHash(); } @@ -128,7 +123,7 @@ public ConsensusHashFinder getHashFinder() { * method returns true, then {@link #getStatus()} will return a value that is not * {@link HashValidityStatus#UNDECIDED}. */ - public synchronized boolean reportSelfHash(@NonNull final Hash selfStateHash) { + public boolean reportSelfHash(@NonNull final Hash selfStateHash) { if (this.selfStateHash != null) { throw new IllegalStateException("self hash reported more than once"); } @@ -152,7 +147,7 @@ public synchronized boolean reportSelfHash(@NonNull final Hash selfStateHash) { * method returns true, then {@link #getStatus()} will return a value that is not * {@link HashValidityStatus#UNDECIDED}. */ - public synchronized boolean reportHashFromNetwork( + public boolean reportHashFromNetwork( @NonNull final NodeId nodeId, final long nodeWeight, @NonNull final Hash stateHash) { Objects.requireNonNull(nodeId, "nodeId must not be null"); Objects.requireNonNull(stateHash, "stateHash must not be null"); @@ -197,7 +192,7 @@ private boolean decide() { * method returns true, then {@link #getStatus()} will return a value that is not * {@link HashValidityStatus#UNDECIDED}. */ - public synchronized boolean outOfTime() { + public boolean outOfTime() { if (status != HashValidityStatus.UNDECIDED) { // Already decided, once decided we don't decide again return false; @@ -236,7 +231,14 @@ public synchronized boolean outOfTime() { * @return a validity status, will be {@link HashValidityStatus#UNDECIDED} until * enough data has been gathered. */ - public synchronized HashValidityStatus getStatus() { + public HashValidityStatus getStatus() { return status; } + + /** + * @return true if there is any disagreement between nodes on the hash for this round + */ + public boolean hasDisagreement() { + return hashFinder.hasDisagreement(); + } } 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 ea085756bab7..4f81eadd333a 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 @@ -19,10 +19,10 @@ import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; import static com.swirlds.platform.system.SystemExitCode.FATAL_ERROR; -import com.swirlds.common.crypto.Hash; import com.swirlds.common.merkle.crypto.MerkleCryptoFactory; import com.swirlds.platform.components.common.output.FatalErrorConsumer; -import com.swirlds.platform.dispatch.triggers.flow.StateHashedTrigger; +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; @@ -43,11 +43,6 @@ public class SignedStateHasher { */ private final SignedStateMetrics signedStateMetrics; - /** - * The StateHashedTrigger to notify if the hashing is successful. - */ - private final StateHashedTrigger stateHashedTrigger; - /** * The FatalErrorConsumer to notify with any fatal errors that occur during hashing. */ @@ -59,20 +54,13 @@ public class SignedStateHasher { * dispatched to the provided StateHashedTrigger. * * @param signedStateMetrics the SignedStateMetrics instance to record time spent hashing. - * @param stateHashedTrigger the StateHashedTrigger dispatcher to notify with hash. * @param fatalErrorConsumer the FatalErrorConsumer to consume any fatal errors during hashing. * - * @throws NullPointerException if any of the following parameters are {@code null}. - *
      - *
    • {@code stateHashedTrigger}
    • - *
    • {@code fatalErrorConsumer}
    • - *
    + * @throws NullPointerException if any of the {@code fatalErrorConsumer} parameter is {@code null}. */ public SignedStateHasher( - SignedStateMetrics signedStateMetrics, - StateHashedTrigger stateHashedTrigger, - FatalErrorConsumer fatalErrorConsumer) { - this.stateHashedTrigger = Objects.requireNonNull(stateHashedTrigger, "stateHashedTrigger must not be null"); + @Nullable final SignedStateMetrics signedStateMetrics, + @NonNull final FatalErrorConsumer fatalErrorConsumer) { this.fatalErrorConsumer = Objects.requireNonNull(fatalErrorConsumer, "fatalErrorConsumer must not be null"); this.signedStateMetrics = signedStateMetrics; } @@ -85,7 +73,7 @@ public SignedStateHasher( public void hashState(final SignedState signedState) { final Instant start = Instant.now(); try { - final Hash hash = MerkleCryptoFactory.getInstance() + MerkleCryptoFactory.getInstance() .digestTreeAsync(signedState.getState()) .get(); @@ -95,8 +83,6 @@ public void hashState(final SignedState signedState) { .update(Duration.between(start, Instant.now()).toMillis()); } - stateHashedTrigger.dispatch(signedState.getRound(), hash); - } catch (final ExecutionException e) { fatalErrorConsumer.fatalError("Exception occurred during SignedState hashing", e, FATAL_ERROR); } catch (final InterruptedException e) { diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/state/notifications/IssNotification.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/state/notifications/IssNotification.java index 8ed25c91a9c5..a9e3fe69de9c 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/state/notifications/IssNotification.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/state/notifications/IssNotification.java @@ -46,27 +46,25 @@ public enum IssType { } private final IssType issType; - private final NodeId otherNodeId; /** * Create a new ISS notification. * * @param round the round when the ISS occurred * @param issType the type of the ISS - * @param otherNodeId the node with an ISS. If this is a {@link IssType#CATASTROPHIC_ISS} then this is null. */ - public IssNotification(final long round, @NonNull final IssType issType, @Nullable final NodeId otherNodeId) { - this.otherNodeId = otherNodeId; + public IssNotification(final long round, @NonNull final IssType issType) { this.issType = Objects.requireNonNull(issType, "issType must not be null"); this.round = round; } /** - * Get the ID of the node that has an ISS. Null if {@link #getIssType()} does not return {@link IssType#OTHER_ISS}. + * @deprecated this method always returns null and will be removed in a future release */ @Nullable + @Deprecated(forRemoval = true) public NodeId getOtherNodeId() { - return otherNodeId; + return null; } /** diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/LinkedEventIntakeWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/LinkedEventIntakeWiring.java index 4fa5309be470..e222eab4d9fa 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/LinkedEventIntakeWiring.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/LinkedEventIntakeWiring.java @@ -34,6 +34,8 @@ * @param eventInput the input wire for events to be added to the hashgraph * @param pauseInput the input wire for pausing the linked event intake * @param consensusRoundOutput the output wire for consensus rounds + * @param consensusEventsOutput the output wire for consensus events, transformed from the consensus round + * output * @param keystoneEventSequenceNumberOutput the output wire for the keystone event sequence number * @param flushRunnable the runnable to flush the intake */ @@ -41,6 +43,7 @@ public record LinkedEventIntakeWiring( @NonNull InputWire eventInput, @NonNull InputWire pauseInput, @NonNull OutputWire consensusRoundOutput, + @NonNull OutputWire> consensusEventsOutput, @NonNull StandardOutputWire keystoneEventSequenceNumberOutput, @NonNull Runnable flushRunnable) { @@ -58,6 +61,7 @@ public static LinkedEventIntakeWiring create(@NonNull final TaskScheduler eventHasherScheduler, @@ -72,7 +77,11 @@ public record PlatformSchedulers( @NonNull TaskScheduler applicationTransactionPrehandlerScheduler, @NonNull TaskScheduler> stateSignatureCollectorScheduler, @NonNull TaskScheduler shadowgraphScheduler, - @NonNull TaskScheduler> futureEventBufferScheduler) { + @NonNull TaskScheduler consensusRoundHandlerScheduler, + @NonNull TaskScheduler eventStreamManagerScheduler, + @NonNull TaskScheduler runningHashUpdateScheduler, + @NonNull TaskScheduler> futureEventBufferScheduler, + @NonNull TaskScheduler> issDetectorScheduler) { /** * Instantiate the schedulers for the platform, for the given wiring model @@ -210,12 +219,39 @@ public static PlatformSchedulers create( .withFlushingEnabled(true) .build() .cast(), + // the literal "consensusRoundHandler" is used by the app to log on the transaction handling thread. + // Do not modify, unless you also change the TRANSACTION_HANDLING_THREAD_NAME constant + model.schedulerBuilder("consensusRoundHandler") + .withType(config.consensusRoundHandlerSchedulerType()) + .withUnhandledTaskCapacity(config.consensusRoundHandlerUnhandledCapacity()) + .withMetricsBuilder(model.metricsBuilder() + .withUnhandledTaskMetricEnabled(true) + .withBusyFractionMetricsEnabled(true)) + .withFlushingEnabled(true) + .build() + .cast(), + // though the eventStreamManager is of DIRECT_STATELESS type, it isn't actually stateless: it just + // is thread safe, and can therefore be treated as if it were stateless by the framework + model.schedulerBuilder("eventStreamManager") + .withType(TaskSchedulerType.DIRECT_STATELESS) + .build() + .cast(), + model.schedulerBuilder("runningHashUpdate") + .withType(TaskSchedulerType.DIRECT_STATELESS) + .build() + .cast(), model.schedulerBuilder("futureEventBuffer") .withType(config.futureEventBufferSchedulerType()) .withUnhandledTaskCapacity(config.futureEventBufferUnhandledCapacity()) .withMetricsBuilder(model.metricsBuilder().withUnhandledTaskMetricEnabled(true)) .withFlushingEnabled(true) .build() + .cast(), + model.schedulerBuilder("issDetector") + .withType(config.issDetectorSchedulerType()) + .withUnhandledTaskCapacity(config.issDetectorUnhandledCapacity()) + .withMetricsBuilder(model.metricsBuilder().withUnhandledTaskMetricEnabled(true)) + .build() .cast()); } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformSchedulersConfig.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformSchedulersConfig.java index 6bde04aabede..9652be9f74d2 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformSchedulersConfig.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformSchedulersConfig.java @@ -78,9 +78,14 @@ * collector * @param shadowgraphSchedulerType the shadowgraph scheduler type * @param shadowgraphUnhandledCapacity number of unhandled tasks allowed for the shadowgraph + * @param consensusRoundHandlerSchedulerType the consensus round handler scheduler type + * @param consensusRoundHandlerUnhandledCapacity number of unhandled tasks allowed for the consensus round + * handler * @param futureEventBufferSchedulerType the future event buffer scheduler type * @param futureEventBufferUnhandledCapacity number of unhandled tasks allowed for the future event * buffer + * @param issDetectorSchedulerType the ISS detector scheduler type + * @param issDetectorUnhandledCapacity number of unhandled tasks allowed for the ISS detector */ @ConfigData("platformSchedulers") public record PlatformSchedulersConfig( @@ -117,5 +122,9 @@ public record PlatformSchedulersConfig( @ConfigProperty(defaultValue = "500") int stateSignatureCollectorUnhandledCapacity, @ConfigProperty(defaultValue = "SEQUENTIAL") TaskSchedulerType shadowgraphSchedulerType, @ConfigProperty(defaultValue = "500") int shadowgraphUnhandledCapacity, + @ConfigProperty(defaultValue = "SEQUENTIAL_THREAD") TaskSchedulerType consensusRoundHandlerSchedulerType, + @ConfigProperty(defaultValue = "5") int consensusRoundHandlerUnhandledCapacity, @ConfigProperty(defaultValue = "SEQUENTIAL") TaskSchedulerType futureEventBufferSchedulerType, - @ConfigProperty(defaultValue = "500") int futureEventBufferUnhandledCapacity) {} + @ConfigProperty(defaultValue = "500") int futureEventBufferUnhandledCapacity, + @ConfigProperty(defaultValue = "SEQUENTIAL") TaskSchedulerType issDetectorSchedulerType, + @ConfigProperty(defaultValue = "500") int issDetectorUnhandledCapacity) {} 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 e103455f2b56..108ef1aba29f 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 @@ -24,6 +24,8 @@ import com.swirlds.base.time.Time; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.io.IOIterator; +import com.swirlds.common.stream.EventStreamManager; +import com.swirlds.common.stream.RunningEventHashUpdate; import com.swirlds.common.utility.Clearable; import com.swirlds.common.wiring.counters.BackpressureObjectCounter; import com.swirlds.common.wiring.counters.ObjectCounter; @@ -49,10 +51,13 @@ import com.swirlds.platform.event.validation.AddressBookUpdate; 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.TransactionPool; import com.swirlds.platform.gossip.shadowgraph.Shadowgraph; import com.swirlds.platform.internal.ConsensusRound; +import com.swirlds.platform.internal.EventImpl; import com.swirlds.platform.state.SwirldStateManager; +import com.swirlds.platform.state.iss.IssDetector; import com.swirlds.platform.state.nexus.LatestCompleteStateNexus; import com.swirlds.platform.state.signed.ReservedSignedState; import com.swirlds.platform.state.signed.SignedStateFileManager; @@ -60,16 +65,20 @@ import com.swirlds.platform.state.signed.StateSignatureCollector; import com.swirlds.platform.system.status.PlatformStatusManager; import com.swirlds.platform.wiring.components.ApplicationTransactionPrehandlerWiring; +import com.swirlds.platform.wiring.components.ConsensusRoundHandlerWiring; import com.swirlds.platform.wiring.components.EventCreationManagerWiring; import com.swirlds.platform.wiring.components.EventDurabilityNexusWiring; import com.swirlds.platform.wiring.components.EventHasherWiring; +import com.swirlds.platform.wiring.components.EventStreamManagerWiring; import com.swirlds.platform.wiring.components.EventWindowManagerWiring; import com.swirlds.platform.wiring.components.FutureEventBufferWiring; import com.swirlds.platform.wiring.components.GossipWiring; +import com.swirlds.platform.wiring.components.IssDetectorWiring; import com.swirlds.platform.wiring.components.PcesReplayerWiring; import com.swirlds.platform.wiring.components.PcesSequencerWiring; import com.swirlds.platform.wiring.components.PcesWriterWiring; import com.swirlds.platform.wiring.components.PostHashCollectorWiring; +import com.swirlds.platform.wiring.components.RunningHashUpdaterWiring; import com.swirlds.platform.wiring.components.ShadowgraphWiring; import com.swirlds.platform.wiring.components.StateSignatureCollectorWiring; import edu.umd.cs.findbugs.annotations.NonNull; @@ -109,11 +118,10 @@ public class PlatformWiring implements Startable, Stoppable, Clearable { private final FutureEventBufferWiring futureEventBufferWiring; private final GossipWiring gossipWiring; private final EventWindowManagerWiring eventWindowManagerWiring; - - /** - * The object counter that spans the event hasher and the post hash collector. - */ - private final ObjectCounter hashingObjectCounter; + private final ConsensusRoundHandlerWiring consensusRoundHandlerWiring; + private final EventStreamManagerWiring eventStreamManagerWiring; + private final RunningHashUpdaterWiring runningHashUpdaterWiring; + private final IssDetectorWiring issDetectorWiring; private final PlatformCoordinator platformCoordinator; @@ -139,7 +147,7 @@ public PlatformWiring(@NonNull final PlatformContext platformContext, @NonNull f // 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. // This counter is the on-ramp for the event hasher, and the off-ramp for the post hash collector. - hashingObjectCounter = new BackpressureObjectCounter( + final ObjectCounter hashingObjectCounter = new BackpressureObjectCounter( "hashingObjectCounter", platformContext .getConfiguration() @@ -170,8 +178,10 @@ public PlatformWiring(@NonNull final PlatformContext platformContext, @NonNull f signedStateFileManagerWiring = SignedStateFileManagerWiring.create(model, schedulers.signedStateFileManagerScheduler()); stateSignerWiring = StateSignerWiring.create(schedulers.stateSignerScheduler()); - shadowgraphWiring = ShadowgraphWiring.create(schedulers.shadowgraphScheduler()); + consensusRoundHandlerWiring = ConsensusRoundHandlerWiring.create(schedulers.consensusRoundHandlerScheduler()); + eventStreamManagerWiring = EventStreamManagerWiring.create(schedulers.eventStreamManagerScheduler()); + runningHashUpdaterWiring = RunningHashUpdaterWiring.create(schedulers.runningHashUpdateScheduler()); platformCoordinator = new PlatformCoordinator( hashingObjectCounter, @@ -184,7 +194,8 @@ public PlatformWiring(@NonNull final PlatformContext platformContext, @NonNull f linkedEventIntakeWiring, eventCreationManagerWiring, applicationTransactionPrehandlerWiring, - stateSignatureCollectorWiring); + stateSignatureCollectorWiring, + consensusRoundHandlerWiring); pcesReplayerWiring = PcesReplayerWiring.create(schedulers.pcesReplayerScheduler()); pcesWriterWiring = PcesWriterWiring.create(schedulers.pcesWriterScheduler()); @@ -194,6 +205,7 @@ public PlatformWiring(@NonNull final PlatformContext platformContext, @NonNull f gossipWiring = GossipWiring.create(model); eventWindowManagerWiring = EventWindowManagerWiring.create(model); + issDetectorWiring = IssDetectorWiring.create(model, schedulers.issDetectorScheduler()); wire(); } @@ -253,13 +265,20 @@ private void wire() { pcesReplayerWiring.doneStreamingPcesOutputWire().solderTo(pcesWriterWiring.doneStreamingPcesInputWire()); pcesReplayerWiring.eventOutput().solderTo(eventHasherWiring.eventInput()); linkedEventIntakeWiring.keystoneEventSequenceNumberOutput().solderTo(pcesWriterWiring.flushRequestInputWire()); + linkedEventIntakeWiring.consensusRoundOutput().solderTo(consensusRoundHandlerWiring.roundInput()); linkedEventIntakeWiring.consensusRoundOutput().solderTo(eventWindowManagerWiring.consensusRoundInput()); + linkedEventIntakeWiring.consensusEventsOutput().solderTo(eventStreamManagerWiring.eventsInput()); pcesWriterWiring .latestDurableSequenceNumberOutput() .solderTo(eventDurabilityNexusWiring.latestDurableSequenceNumber()); signedStateFileManagerWiring .oldestMinimumGenerationOnDiskOutputWire() .solderTo(pcesWriterWiring.minimumAncientIdentifierToStoreInputWire()); + + runningHashUpdaterWiring + .runningHashUpdateOutput() + .solderTo(consensusRoundHandlerWiring.runningHashUpdateInput()); + runningHashUpdaterWiring.runningHashUpdateOutput().solderTo(eventStreamManagerWiring.runningHashUpdateInput()); } /** @@ -314,7 +333,10 @@ public void wireExternalComponents( * @param eventCreationManager the event creation manager to bind * @param swirldStateManager the swirld state manager to bind * @param stateSignatureCollector the signed state manager to bind + * @param consensusRoundHandler the consensus round handler to bind + * @param eventStreamManager the event stream manager to bind * @param futureEventBuffer the future event buffer to bind + * @param issDetector the ISS detector to bind */ public void bind( @NonNull final EventHasher eventHasher, @@ -334,7 +356,10 @@ public void bind( @NonNull final EventCreationManager eventCreationManager, @NonNull final SwirldStateManager swirldStateManager, @NonNull final StateSignatureCollector stateSignatureCollector, - @NonNull final FutureEventBuffer futureEventBuffer) { + @NonNull final ConsensusRoundHandler consensusRoundHandler, + @NonNull final EventStreamManager eventStreamManager, + @NonNull final FutureEventBuffer futureEventBuffer, + @NonNull final IssDetector issDetector) { eventHasherWiring.bind(eventHasher); internalEventValidatorWiring.bind(internalEventValidator); @@ -353,7 +378,10 @@ public void bind( eventCreationManagerWiring.bind(eventCreationManager); applicationTransactionPrehandlerWiring.bind(swirldStateManager); stateSignatureCollectorWiring.bind(stateSignatureCollector); + consensusRoundHandlerWiring.bind(consensusRoundHandler); + eventStreamManagerWiring.bind(eventStreamManager); futureEventBufferWiring.bind(futureEventBuffer); + issDetectorWiring.bind(issDetector); } /** @@ -474,13 +502,17 @@ public InputWire getPcesWriterRegisterDiscontinuityInput() { } /** - * Get a supplier for the number of unprocessed tasks in the hasher. + * Get a supplier for the number of unprocessed tasks at the front of the intake pipeline. This is for the purpose + * of applying backpressure to the event creator and gossip when the intake pipeline is overloaded. + *

    + * Technically, the first component of the intake pipeline is the hasher, but tasks to be passed along actually + * accumulate in the post hash collector. This is due to how the concurrent hasher handles backpressure. * - * @return a supplier for the number of unprocessed tasks in the hasher + * @return a supplier for the number of unprocessed tasks in the PostHashCollector */ @NonNull - public LongSupplier getHasherUnprocessedTaskCountSupplier() { - return eventHasherWiring.unprocessedTaskCountSupplier(); + public LongSupplier getIntakeQueueSizeSupplier() { + return postHashCollectorWiring.unprocessedTaskCountSupplier(); } /** @@ -494,6 +526,22 @@ public StandardOutputWire getKeystoneEventSequenceNumberOutput() { return linkedEventIntakeWiring.keystoneEventSequenceNumberOutput(); } + /** + * Update the running hash for all components that need it. + * + * @param runningHashUpdate the object containing necessary information to update the running hash + */ + public void updateRunningHash(@NonNull final RunningEventHashUpdate runningHashUpdate) { + runningHashUpdaterWiring.runningHashUpdateInput().inject(runningHashUpdate); + } + + /** + * @return the wiring wrapper for the ISS detector + */ + public @NonNull IssDetectorWiring getIssDetectorWiring() { + return issDetectorWiring; + } + /** * Inject a new non-ancient event window into all components that need it. * @@ -510,6 +558,13 @@ public void flushIntakePipeline() { platformCoordinator.flushIntakePipeline(); } + /** + * Flush the consensus round handler. + */ + public void flushConsensusRoundHandler() { + consensusRoundHandlerWiring.flushRunnable().run(); + } + /** * {@inheritDoc} */ diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/ConsensusRoundHandlerWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/ConsensusRoundHandlerWiring.java new file mode 100644 index 000000000000..85823967708d --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/ConsensusRoundHandlerWiring.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.swirlds.platform.wiring.components; + +import com.swirlds.common.stream.RunningEventHashUpdate; +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.platform.eventhandling.ConsensusRoundHandler; +import com.swirlds.platform.internal.ConsensusRound; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Wiring for the {@link com.swirlds.platform.eventhandling.ConsensusRoundHandler} + * + * @param roundInput the input wire for consensus rounds to be applied to the state + * @param runningHashUpdateInput the input wire for updating the running event hash + * @param flushRunnable the runnable to flush the task scheduler + */ +public record ConsensusRoundHandlerWiring( + @NonNull InputWire roundInput, + @NonNull InputWire runningHashUpdateInput, + @NonNull Runnable flushRunnable) { + /** + * Create a new instance of this wiring. + * + * @param taskScheduler the task scheduler for this wiring object + * @return the new wiring instance + */ + @NonNull + public static ConsensusRoundHandlerWiring create(@NonNull final TaskScheduler taskScheduler) { + return new ConsensusRoundHandlerWiring( + taskScheduler.buildInputWire("rounds"), + taskScheduler.buildInputWire("running hash update"), + taskScheduler::flush); + } + + /** + * Bind the consensus round handler to this wiring. + * + * @param consensusRoundHandler the consensus round handler to bind + */ + public void bind(@NonNull final ConsensusRoundHandler consensusRoundHandler) { + ((BindableInputWire) roundInput).bind(consensusRoundHandler::handleConsensusRound); + ((BindableInputWire) runningHashUpdateInput) + .bind(consensusRoundHandler::updateRunningHash); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/EventHasherWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/EventHasherWiring.java index db35a23c08a6..d4685b8bc93c 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/EventHasherWiring.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/EventHasherWiring.java @@ -23,19 +23,15 @@ import com.swirlds.platform.event.GossipEvent; import com.swirlds.platform.event.hashing.EventHasher; import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.function.LongSupplier; /** * Wiring for the {@link EventHasher}. * - * @param eventInput the input wire for events to be hashed - * @param eventOutput the output wire for hashed events - * @param unprocessedTaskCountSupplier the supplier for the number of unprocessed tasks + * @param eventInput the input wire for events to be hashed + * @param eventOutput the output wire for hashed events */ public record EventHasherWiring( - @NonNull InputWire eventInput, - @NonNull OutputWire eventOutput, - @NonNull LongSupplier unprocessedTaskCountSupplier) { + @NonNull InputWire eventInput, @NonNull OutputWire eventOutput) { /** * Create a new instance of this wiring. * @@ -43,10 +39,7 @@ public record EventHasherWiring( * @return the new wiring instance */ public static EventHasherWiring create(@NonNull final TaskScheduler taskScheduler) { - return new EventHasherWiring( - taskScheduler.buildInputWire("events to hash"), - taskScheduler.getOutputWire(), - taskScheduler::getUnprocessedTaskCount); + return new EventHasherWiring(taskScheduler.buildInputWire("events to hash"), taskScheduler.getOutputWire()); } /** diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/EventStreamManagerWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/EventStreamManagerWiring.java new file mode 100644 index 000000000000..ed664decb0bc --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/EventStreamManagerWiring.java @@ -0,0 +1,60 @@ +/* + * 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.stream.EventStreamManager; +import com.swirlds.common.stream.RunningEventHashUpdate; +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.platform.internal.EventImpl; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; + +/** + * The wiring for the {@link EventStreamManager} + * + * @param eventsInput the input wire for consensus events + * @param runningHashUpdateInput the input wire to update the running hash upon reconnect or restart + */ +public record EventStreamManagerWiring( + @NonNull InputWire> eventsInput, + @NonNull InputWire runningHashUpdateInput) { + + /** + * Create a new wiring object + * + * @param taskScheduler the task scheduler to use + * @return the new wiring object + */ + @NonNull + public static EventStreamManagerWiring create(@NonNull final TaskScheduler taskScheduler) { + return new EventStreamManagerWiring( + taskScheduler.buildInputWire("events"), taskScheduler.buildInputWire("running hash update")); + } + + /** + * Bind the {@link EventStreamManager} to this wiring + * + * @param eventStreamManager the event stream manager to bind + */ + public void bind(@NonNull final EventStreamManager eventStreamManager) { + ((BindableInputWire, Void>) eventsInput).bind(eventStreamManager::addEvents); + ((BindableInputWire) runningHashUpdateInput) + .bind(eventStreamManager::updateRunningHash); + } +} 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 new file mode 100644 index 000000000000..33350ec7692a --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/IssDetectorWiring.java @@ -0,0 +1,100 @@ +/* + * 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.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.SystemTransactionExtractor; +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; + +/** + * 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 + */ +public record IssDetectorWiring( + @NonNull InputWire endOfPcesReplay, + @NonNull InputWire roundCompletedInput, + @NonNull InputWire handleConsensusRound, + @NonNull InputWire>> handlePostconsensusSignatures, + @NonNull InputWire newStateHashed, + @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", + new SystemTransactionExtractor<>(StateSignatureTransaction.class)::handleRound); + final InputWire>> sigInput = + taskScheduler.buildInputWire("handlePostconsensusSignatures"); + roundTransformer.getOutputWire().solderTo(sigInput); + return new IssDetectorWiring( + taskScheduler.buildInputWire("endOfPcesReplay"), + taskScheduler.buildInputWire("roundCompleted"), + roundTransformer.getInputWire(), + sigInput, + taskScheduler.buildInputWire("newStateHashed"), + taskScheduler.buildInputWire("overridingState"), + taskScheduler.getOutputWire().buildSplitter("issNotificationSplitter", "issNotificationList")); + } + + /** + * Bind the given ISS detector to this wiring. + * + * @param issDetector the ISS detector + */ + 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>) overridingState) + .bind(issDetector::overridingState); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/PostHashCollectorWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/PostHashCollectorWiring.java index f5c869b300d3..ad1a7499995b 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/PostHashCollectorWiring.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/PostHashCollectorWiring.java @@ -22,6 +22,7 @@ import com.swirlds.common.wiring.wires.output.OutputWire; import com.swirlds.platform.event.GossipEvent; import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.function.LongSupplier; /** * Wiring object that allows for the staging of events that have been hashed, but haven't been passed further down the @@ -41,11 +42,14 @@ * The concurrent scheduler will refuse to accept additional work based on the number of tasks that are waiting in the * sequential scheduler's queue. * - * @param eventInput the input wire for events that have been hashed - * @param eventOutput the output wire for events to be passed further along the pipeline + * @param eventInput the input wire for events that have been hashed + * @param eventOutput the output wire for events to be passed further along the pipeline + * @param unprocessedTaskCountSupplier the supplier for the number of unprocessed tasks */ public record PostHashCollectorWiring( - @NonNull InputWire eventInput, @NonNull OutputWire eventOutput) { + @NonNull InputWire eventInput, + @NonNull OutputWire eventOutput, + @NonNull LongSupplier unprocessedTaskCountSupplier) { /** * Create a new instance of this wiring. @@ -60,6 +64,7 @@ public static PostHashCollectorWiring create(@NonNull final TaskScheduler hashedEvent); - return new PostHashCollectorWiring(inputWire, taskScheduler.getOutputWire()); + return new PostHashCollectorWiring( + inputWire, taskScheduler.getOutputWire(), taskScheduler::getUnprocessedTaskCount); } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/RunningHashUpdaterWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/RunningHashUpdaterWiring.java new file mode 100644 index 000000000000..c6c5ccbf79b4 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/components/RunningHashUpdaterWiring.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.platform.wiring.components; + +import com.swirlds.common.stream.RunningEventHashUpdate; +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; + +/** + * A wiring object for distributing {@link RunningEventHashUpdate}s + * + * @param runningHashUpdateInput the input wire for running hash updates to be distributed + * @param runningHashUpdateOutput the output wire for running hash updates to be distributed + */ +public record RunningHashUpdaterWiring( + @NonNull InputWire runningHashUpdateInput, + @NonNull OutputWire runningHashUpdateOutput) { + + /** + * Create a new wiring object + * + * @param taskScheduler the task scheduler to use + * @return the new wiring object + */ + @NonNull + public static RunningHashUpdaterWiring create(@NonNull final TaskScheduler taskScheduler) { + final BindableInputWire inputWire = + taskScheduler.buildInputWire("running hash update"); + final RunningHashUpdaterWiring wiring = new RunningHashUpdaterWiring(inputWire, taskScheduler.getOutputWire()); + + // this is just a pass through method + inputWire.bind(runningHashUpdate -> runningHashUpdate); + + return wiring; + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/diagram-commands.txt b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/diagram-commands.txt deleted file mode 100644 index 46a1bceb8aec..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/diagram-commands.txt +++ /dev/null @@ -1,59 +0,0 @@ -######################################################################################################################## -#### Slight Collapsing - -Groups high level grouping of components and component groups. Substitutes known spammy wires. - -pcli diagram \ - -l 'applicationTransactionPrehandler:futures:linkedEventIntake' \ - -s 'eventWindowManager:non-ancient event window:ʘ' \ - -s 'heartbeat:heartbeat:♡' \ - -s 'eventCreationManager:non-validated events:†' \ - -s 'applicationTransactionPrehandler:futures:★' \ - -s 'pcesReplayer:done streaming pces:@' \ - -s 'inOrderLinker:events to gossip:g' \ - -g 'Event Validation:internalEventValidator,eventDeduplicator,eventSignatureValidator' \ - -g 'Event Hashing:eventHasher,postHashCollector' \ - -g 'Orphan Buffer:orphanBuffer,orphanBufferSplitter' \ - -g 'Linked Event Intake:linkedEventIntake,linkedEventIntakeSplitter' \ - -g 'State File Management:saveToDiskFilter,signedStateFileManager,extractOldestMinimumGenerationOnDisk,toStateWrittenToDiskAction' \ - -g 'State Signature Collection:stateSignatureCollector,reservedStateSplitter,allStatesReserver,completeStateFilter,completeStatesReserver,extractConsensusSignatureTransactions,extractPreconsensusSignatureTransactions' \ - -g 'Intake Pipeline:Event Validation,Orphan Buffer,Event Hashing' \ - -g 'PCES:pcesSequencer,pcesWriter,eventDurabilityNexus' \ - -g 'Consensus Pipeline:inOrderLinker,Linked Event Intake,g' \ - -g 'Event Creation:futureEventBuffer,futureEventBufferSplitter,eventCreationManager' \ - -g 'Gossip:gossip,shadowgraph' \ - -c 'Orphan Buffer' - -######################################################################################################################## -#### Heavy Collapsing - -Same as 'Uncollapsed' but with low level things collapsed. Attempts to hide things like transformers and splitters. - -pcli diagram \ - -l 'applicationTransactionPrehandler:futures:linkedEventIntake' \ - -s 'eventWindowManager:non-ancient event window:ʘ' \ - -s 'heartbeat:heartbeat:♡' \ - -s 'eventCreationManager:non-validated events:†' \ - -s 'applicationTransactionPrehandler:futures:★' \ - -s 'pcesReplayer:done streaming pces:@' \ - -s 'extractOldestMinimumGenerationOnDisk:minimum identifier to store:s' \ - -s 'inOrderLinker:events to gossip:g' \ - -g 'Event Validation:internalEventValidator,eventDeduplicator,eventSignatureValidator' \ - -g 'Event Hashing:eventHasher,postHashCollector' \ - -g 'Orphan Buffer:orphanBuffer,orphanBufferSplitter' \ - -g 'Linked Event Intake:linkedEventIntake,linkedEventIntakeSplitter' \ - -g 'State File Management:saveToDiskFilter,signedStateFileManager,extractOldestMinimumGenerationOnDisk,toStateWrittenToDiskAction' \ - -g 'State Signature Collection:stateSignatureCollector,reservedStateSplitter,allStatesReserver,completeStateFilter,completeStatesReserver,extractConsensusSignatureTransactions,extractPreconsensusSignatureTransactions' \ - -g 'Intake Pipeline:Event Validation,Orphan Buffer,Event Hashing' \ - -g 'PCES Writer:pcesSequencer,pcesWriter,eventDurabilityNexus' \ - -g 'Consensus Pipeline:inOrderLinker,Linked Event Intake,g' \ - -g 'Event Creation:futureEventBuffer,futureEventBufferSplitter,eventCreationManager' \ - -g 'Gossip:gossip,shadowgraph' \ - -c 'Orphan Buffer' \ - -c 'Linked Event Intake' \ - -c 'State File Management' \ - -c 'State Signature Collection' \ - -c 'State Signature Collection' \ - -c 'State File Management' \ - -c 'Event Hashing' \ - -c 'PCES Writer' 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 new file mode 100755 index 000000000000..bcfb05c61b2a --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/generate-platform-diagram.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +pcli diagram \ + -l 'applicationTransactionPrehandler:futures:consensusRoundHandler' \ + -l 'eventDurabilityNexus:wait for durability:consensusRoundHandler' \ + -s 'eventWindowManager:non-ancient event window:ʘ' \ + -s 'heartbeat:heartbeat:♡' \ + -s 'eventCreationManager:non-validated events:†' \ + -s 'applicationTransactionPrehandler:futures:★' \ + -s 'eventDurabilityNexus:wait for durability:🕑' \ + -s 'pcesReplayer:done streaming pces:@' \ + -s 'inOrderLinker:events to gossip:g' \ + -s 'runningHashUpdate:running hash update:§' \ + -s 'linkedEventIntake:flush request:Ξ' \ + -g 'Event Validation:internalEventValidator,eventDeduplicator,eventSignatureValidator' \ + -g 'Event Hashing:eventHasher,postHashCollector' \ + -g 'Orphan Buffer:orphanBuffer,orphanBufferSplitter' \ + -g 'Linked Event Intake:linkedEventIntake,linkedEventIntakeSplitter,eventWindowManager' \ + -g 'State File Management:saveToDiskFilter,signedStateFileManager,extractOldestMinimumGenerationOnDisk,toStateWrittenToDiskAction' \ + -g 'State Signature Collection:stateSignatureCollector,reservedStateSplitter,allStatesReserver,completeStateFilter,completeStatesReserver,extractConsensusSignatureTransactions,extractPreconsensusSignatureTransactions' \ + -g 'Intake Pipeline:Event Validation,Orphan Buffer,Event Hashing' \ + -g 'Preconsensus Event Stream:pcesSequencer,pcesWriter,eventDurabilityNexus' \ + -g 'Consensus Event Stream:getEvents,eventStreamManager' \ + -g 'Consensus Pipeline:inOrderLinker,Linked Event Intake,g' \ + -g 'Event Creation:futureEventBuffer,futureEventBufferSplitter,eventCreationManager' \ + -g 'Gossip:gossip,shadowgraph' \ + -c 'Consensus Event Stream' \ + -c 'Orphan Buffer' \ + -c 'Linked Event Intake' \ + -c 'State Signature Collection' \ + -c 'State File Management' 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 7dfffad0643b..4ed25095583b 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 @@ -101,10 +101,6 @@ com.hedera.node.test.clients; exports com.swirlds.platform.dispatch.triggers.error to com.swirlds.platform.test; - exports com.swirlds.platform.dispatch.triggers.flow to - com.swirlds.platform.test; - exports com.swirlds.platform.dispatch.triggers.transaction 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/components/appcomm/AppCommComponentTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/appcomm/AppCommComponentTests.java index e2657a457665..9d6a6941d55f 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/appcomm/AppCommComponentTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/components/appcomm/AppCommComponentTests.java @@ -24,7 +24,6 @@ import com.swirlds.common.context.PlatformContext; import com.swirlds.common.notification.NotificationEngine; -import com.swirlds.common.platform.NodeId; import com.swirlds.common.test.fixtures.RandomUtils; import com.swirlds.common.test.fixtures.ResettableRandom; import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; @@ -32,15 +31,11 @@ import com.swirlds.platform.state.RandomSignedStateGenerator; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.state.signed.StateSavingResult; -import com.swirlds.platform.system.state.notifications.IssListener; -import com.swirlds.platform.system.state.notifications.IssNotification; import com.swirlds.platform.system.state.notifications.NewSignedStateListener; import java.time.Duration; -import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; /** @@ -119,28 +114,4 @@ void testNewLatestCompleteStateEventNotification() throws InterruptedException { -1, signedState::getReservationCount, Duration.ofSeconds(1), "Signed state should be fully released"); assertEquals(1, numInvocations.get(), "Unexpected number of notification callbacks"); } - - @RepeatedTest(100) - @DisplayName("IssNotification") - void testIssNotification() { - final Random random = RandomUtils.getRandomPrintSeed(); - final long round = random.nextLong(); - final int numTypes = IssNotification.IssType.values().length; - final IssNotification.IssType issType = IssNotification.IssType.values()[random.nextInt(numTypes)]; - final NodeId otherNodeId = random.nextDouble() > 0.8 ? null : new NodeId(Math.abs(random.nextLong())); - - final AtomicInteger numInvocations = new AtomicInteger(); - final NotificationEngine notificationEngine = NotificationEngine.buildEngine(getStaticThreadManager()); - notificationEngine.register(IssListener.class, n -> { - numInvocations.getAndIncrement(); - assertEquals(round, n.getRound(), "Unexpected ISS round"); - assertEquals(issType, n.getIssType(), "Unexpected ISS Type"); - assertEquals(otherNodeId, n.getOtherNodeId(), "Unexpected other node id"); - }); - - final AppCommunicationComponent component = new AppCommunicationComponent(notificationEngine, context); - component.iss(round, issType, otherNodeId); - - assertEquals(1, numInvocations.get(), "Unexpected number of notification callbacks"); - } } 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 d066696f1ec4..dfe0086ffee5 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 @@ -221,7 +221,6 @@ private DefaultStateManagementComponent newStateManagementComponent( final DefaultStateManagementComponent stateManagementComponent = new DefaultStateManagementComponent( platformContext, AdHocThreadManager.getStaticThreadManager(), - dispatchBuilder, (msg, t, code) -> {}, signer, ss -> {}, diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/config/legacy/LegacyConfigPropertiesLoaderTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/config/legacy/LegacyConfigPropertiesLoaderTest.java index e180a219d01e..46b50281f948 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/config/legacy/LegacyConfigPropertiesLoaderTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/config/legacy/LegacyConfigPropertiesLoaderTest.java @@ -72,10 +72,9 @@ void testRealisticConfig() throws UnknownHostException { Assertions.assertEquals("123", properties.swirldName().get()); Assertions.assertTrue(properties.appConfig().isPresent(), "Value must be set"); - Assertions.assertEquals( - "HashgraphDemo.jar", properties.appConfig().get().jarName()); + Assertions.assertEquals("StatsDemo.jar", properties.appConfig().get().jarName()); Assertions.assertArrayEquals( - new String[] {"1", "0", "0", "0", "0", "0", "0", "0", "0", "0", "all"}, + new String[] {"1", "3000", "0", "100", "-1", "200"}, properties.appConfig().get().params()); final AddressBook addressBook = properties.getAddressBook(); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/AbstractEventHandlerTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/AbstractEventHandlerTests.java deleted file mode 100644 index c2954c298e5a..000000000000 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/AbstractEventHandlerTests.java +++ /dev/null @@ -1,112 +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.eventhandling; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.swirlds.common.metrics.noop.NoOpMetrics; -import com.swirlds.common.platform.NodeId; -import com.swirlds.metrics.api.Metrics; -import com.swirlds.platform.internal.ConsensusRound; -import com.swirlds.platform.internal.EventImpl; -import com.swirlds.platform.metrics.ConsensusHandlingMetrics; -import com.swirlds.platform.metrics.ConsensusMetrics; -import com.swirlds.platform.metrics.SwirldStateMetrics; -import com.swirlds.platform.state.State; -import com.swirlds.platform.stats.CycleTimingStat; -import com.swirlds.platform.system.transaction.ConsensusTransactionImpl; -import com.swirlds.platform.system.transaction.SwirldTransaction; -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import java.util.concurrent.ThreadLocalRandom; -import java.util.function.BiConsumer; -import java.util.function.Supplier; - -public abstract class AbstractEventHandlerTests { - - private static final int NUM_NODES = 10; - - protected NodeId selfId; - protected Metrics metrics; - protected SwirldStateMetrics ssStats; - protected ConsensusMetrics consensusMetrics; - protected ConsensusHandlingMetrics consensusHandlingMetrics; - protected BiConsumer consensusSystemTransactionManager; - protected Supplier consEstimateSupplier; - protected Random random; - - protected void setup() { - selfId = new NodeId(0L); - metrics = new NoOpMetrics(); - ssStats = mock(SwirldStateMetrics.class); - consensusMetrics = mock(ConsensusMetrics.class); - consensusHandlingMetrics = mock(ConsensusHandlingMetrics.class); - when(consensusHandlingMetrics.getConsCycleStat()).thenReturn(mock(CycleTimingStat.class)); - consensusSystemTransactionManager = (s, r) -> {}; - consEstimateSupplier = Instant::now; - random = ThreadLocalRandom.current(); - } - - /** - * Create mock events, some with mock transactions and some with no transactions. - * - * @param numEvents - * the number of events to create - * @param numTransactions - * the number of transactions in each event that has transactions - * @param includeEmptyEvents - * true if empty events should be created - * @return list of events - */ - protected List createEvents( - final int numEvents, final int numTransactions, final boolean includeEmptyEvents) { - final List events = new ArrayList<>(numEvents); - int numEmptyEvents = 0; - if (includeEmptyEvents) { - numEmptyEvents = numEvents / 3; - } - for (int i = 0; i < numEvents; i++) { - final EventImpl event = mock(EventImpl.class); - if (i > numEmptyEvents) { - addTransactionsToEvent(event, numTransactions); - } else { - when(event.getTransactions()).thenReturn(new ConsensusTransactionImpl[0]); - when(event.isEmpty()).thenReturn(true); - } - when(event.getCreatorId()).thenReturn(new NodeId(random.nextInt(NUM_NODES))); - when(event.getConsensusTimestamp()).thenReturn(Instant.now()); - final boolean isConsensus = random.nextBoolean(); - when(event.isConsensus()).thenReturn(isConsensus); - if (isConsensus) { - when(event.getConsensusTimestamp()).thenReturn(Instant.now()); - } - events.add(event); - } - return events; - } - - private void addTransactionsToEvent(final EventImpl event, final int numTransactions) { - final SwirldTransaction[] tx = new SwirldTransaction[numTransactions]; - for (int j = 0; j < numTransactions; j++) { - tx[j] = mock(SwirldTransaction.class); - } - when(event.getTransactions()).thenReturn(tx); - } -} diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/ConsensusQueueTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/ConsensusQueueTests.java deleted file mode 100644 index 42231ffd1073..000000000000 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/ConsensusQueueTests.java +++ /dev/null @@ -1,248 +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.eventhandling; - -import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; -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.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.platform.NodeId; -import com.swirlds.common.test.fixtures.AssertionUtils; -import com.swirlds.common.threading.framework.QueueThread; -import com.swirlds.common.threading.framework.config.QueueThreadConfiguration; -import com.swirlds.platform.internal.ConsensusRound; -import com.swirlds.platform.internal.EventImpl; -import com.swirlds.platform.metrics.ConsensusHandlingMetrics; -import java.time.Duration; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class ConsensusQueueTests { - - private static final NodeId SELF_ID = new NodeId(0L); - - private static final int EVENT_CAPACITY = 100; - public static final String PUT_ERROR = "put() did not update eventsInQueue."; - - private QueueThread queueThread; - - private final AtomicInteger roundsHandled = new AtomicInteger(0); - - private static ConsensusQueue newQueue() { - return new ConsensusQueue(mock(ConsensusHandlingMetrics.class), EVENT_CAPACITY); - } - - @BeforeEach - public void setup() { - queueThread = new QueueThreadConfiguration(getStaticThreadManager()) - .setQueue(newQueue()) - .setNodeId(SELF_ID) - .setHandler(this::handleRound) - .build(); - } - - private int getNumEventsInQueue() { - return queueThread.size(); - } - - /** - * Tests that a single round with a number of events that exceeds the event capacity can be added only when the - * queue is empty. - * - * @throws InterruptedException - * if this thread is interrupted - */ - @Test - void testPutLargeRoundEmptyQueue() throws InterruptedException { - final ConsensusRound bigRound = createRound(EVENT_CAPACITY + 1); - - queueThread.put(bigRound); - - assertEquals(bigRound.getNumEvents(), queueThread.size(), PUT_ERROR); - - final ExecutorService executorService = Executors.newSingleThreadExecutor(); - final Future future = executorService.submit(() -> { - queueThread.put(bigRound); - return null; - }); - - boolean putBlocked = false; - try { - future.get(100, TimeUnit.MILLISECONDS); - } catch (ExecutionException | TimeoutException | InterruptedException e) { - putBlocked = true; - } - - assertTrue( - putBlocked, - "Adding a round with more events than the" + " event capacity should block until the queue is empty."); - } - - /** - * Test that the q2 stat is updated when rounds are added and removed. - * - * @throws InterruptedException - * if this thread is interrupted - */ - @Test - void testQ2StatUpdated() throws InterruptedException { - final ConsensusRound smallRound = createRound(10); - - // The queue is empty. Add a round that will not exceed capacity - queueThread.put(smallRound); - assertEquals(smallRound.getNumEvents(), queueThread.size(), PUT_ERROR); - - // Add the round again and check the stats - queueThread.put(smallRound); - assertEquals(smallRound.getNumEvents() * 2, queueThread.size(), PUT_ERROR); - - queueThread.start(); - - AssertionUtils.assertEventuallyEquals( - 2, roundsHandled::get, Duration.ofMillis(1000), "Rounds were not handled"); - assertEquals(0, getNumEventsInQueue(), "eventsInQueue was not updated when a consensus was removed."); - } - - /** - * Test that adding another round is blocked when adding that round would exceed the event capacity. - * - * @throws InterruptedException - * if this thread is interrupted - */ - @Test - void testBlockWhenQueueFull() throws InterruptedException { - final ConsensusRound bigRound = createRound(EVENT_CAPACITY - 10); - final ConsensusRound smallRound = createRound(10); - - // The queue is empty. Add a that will not exceed capacity - queueThread.put(bigRound); - assertEquals(EVENT_CAPACITY - 10, queueThread.size(), PUT_ERROR); - - // Try adding the big round again. It should block because the number of events would exceed the capacity. - final ExecutorService executorService = Executors.newSingleThreadExecutor(); - final Future future = executorService.submit(() -> { - queueThread.put(bigRound); - return null; - }); - - boolean putBlocked = false; - try { - future.get(100, TimeUnit.MILLISECONDS); - } catch (ExecutionException | TimeoutException | InterruptedException e) { - putBlocked = true; - } - - assertTrue( - putBlocked, - "Adding a with events that would exceed" + " event capacity should block until the queue is empty."); - - // Should succeed, because the number of events does not exceed the capacity - queueThread.put(smallRound); - - assertEquals(bigRound.getNumEvents() + smallRound.getNumEvents(), queueThread.size(), PUT_ERROR); - } - - @Test - void testPoll() throws InterruptedException { - assertNull(queueThread.poll()); - assertEquals(0, queueThread.size(), "eventsInQueue should not be modified when the queue is already empty."); - queueThread.put(createRound(15)); - assertEquals(15, queueThread.size(), PUT_ERROR); - assertNotNull(queueThread.poll()); - assertEquals(0, queueThread.size(), "poll() did not update eventsInQueue."); - } - - @Test - void testPollWithTimeout() throws InterruptedException { - assertNull(queueThread.poll(10, TimeUnit.MILLISECONDS)); - assertEquals(0, queueThread.size(), "eventsInQueue should not be modified when the queue is already empty."); - queueThread.put(createRound(15)); - assertEquals(15, queueThread.size(), PUT_ERROR); - assertNotNull(queueThread.poll(10, TimeUnit.MILLISECONDS)); - assertEquals(0, queueThread.size(), "poll() did not update eventsInQueue."); - } - - @Test - void testOffer() throws InterruptedException { - queueThread.put(createRound(100)); - assertFalse(queueThread.offer(createRound(1)), "offer() did not respect the event capacity."); - - queueThread.clear(); - queueThread.put(createRound(10)); - assertEquals(10, queueThread.size(), PUT_ERROR); - assertTrue(queueThread.offer(createRound(2))); - assertEquals(12, queueThread.size(), "offer() did not update eventsInQueue."); - } - - @Test - void testOfferWithTimeout() throws InterruptedException { - queueThread.offer(createRound(10), 100, TimeUnit.MILLISECONDS); - assertEquals(10, queueThread.size(), "offer() did not update eventsInQueue."); - - queueThread.put(createRound(80)); - assertEquals(90, queueThread.size(), PUT_ERROR); - - assertFalse( - queueThread.offer(createRound(20), 100, TimeUnit.MILLISECONDS), - "offer(timeout, unit) should return false if no room is available after the timeout."); - assertEquals( - 90, - queueThread.size(), - "offer(timeout, unit) should not change the eventsInQueue number when nothing was inserted."); - } - - @Test - void testRemove() throws InterruptedException { - assertThrows( - NoSuchElementException.class, - () -> queueThread.remove(), - "Attempting to remove from an empty queue should throw an exception."); - final ConsensusRound round = createRound(10); - queueThread.put(round); - assertEquals(10, queueThread.size(), PUT_ERROR); - assertEquals(round, queueThread.remove(), "remove() did not supply the correct round instance."); - - assertEquals(0, queueThread.size(), "remove() did not update eventsInQueue."); - } - - void handleRound(final ConsensusRound round) { - roundsHandled.addAndGet(1); - } - - private ConsensusRound createRound(final int numEvents) { - final ConsensusRound round = mock(ConsensusRound.class); - final List eventList = mock(List.class); - when(round.getNumEvents()).thenReturn(numEvents); - when(round.getConsensusEvents()).thenReturn(eventList); - return round; - } -} diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/ConsensusRoundHandlerTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/ConsensusRoundHandlerTests.java index e67a6bb90e4a..b16ba0646645 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/ConsensusRoundHandlerTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/eventhandling/ConsensusRoundHandlerTests.java @@ -16,211 +16,187 @@ package com.swirlds.platform.eventhandling; -import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; -import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.swirlds.base.function.CheckedConsumer; +import com.swirlds.base.time.Time; import com.swirlds.common.context.PlatformContext; -import com.swirlds.common.stream.EventStreamManager; -import com.swirlds.common.test.fixtures.RandomAddressBookGenerator; -import com.swirlds.common.test.fixtures.junit.tags.TestQualifierTags; +import com.swirlds.common.crypto.Hash; +import com.swirlds.common.crypto.RunningHash; import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; -import com.swirlds.common.threading.framework.QueueThread; -import com.swirlds.common.threading.utility.ThrowingRunnable; -import com.swirlds.config.api.Configuration; -import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; +import com.swirlds.common.threading.futures.StandardFuture; +import com.swirlds.platform.event.GossipEvent; import com.swirlds.platform.internal.ConsensusRound; import com.swirlds.platform.internal.EventImpl; -import com.swirlds.platform.metrics.SwirldStateMetrics; import com.swirlds.platform.state.PlatformState; import com.swirlds.platform.state.State; import com.swirlds.platform.state.SwirldStateManager; import com.swirlds.platform.state.signed.ReservedSignedState; -import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.SwirldState; -import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.status.StatusActionSubmitter; -import com.swirlds.platform.test.fixtures.state.DummySwirldState; -import java.time.Duration; -import java.time.temporal.ChronoUnit; +import com.swirlds.platform.system.status.actions.FreezePeriodEnteredAction; +import edu.umd.cs.findbugs.annotations.NonNull; import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.RepeatedTest; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -class ConsensusRoundHandlerTests extends AbstractEventHandlerTests { - - private EventStreamManager eventStreamManager; - private QueueThread stateHashSignQueue; - - private ConsensusRoundHandler consensusRoundHandler; +/** + * Unit tests for {@link ConsensusRoundHandler}. + */ +class ConsensusRoundHandlerTests { + private ConsensusRound mockConsensusRound( + @NonNull final EventImpl keystoneEvent, @NonNull final List events, final long roundNumber) { + final ConsensusRound consensusRound = mock(ConsensusRound.class); + when(consensusRound.getConsensusEvents()).thenReturn(events); + when(consensusRound.getConsensusTimestamp()) + .thenReturn(Time.getCurrent().now()); + when(consensusRound.getKeystoneEvent()).thenReturn(keystoneEvent); + when(consensusRound.getRoundNum()).thenReturn(roundNumber); + when(consensusRound.isEmpty()).thenReturn(events.isEmpty()); + + return consensusRound; + } - @Override - @BeforeEach - public void setup() { - super.setup(); - eventStreamManager = mock(EventStreamManager.class); - stateHashSignQueue = mock(QueueThread.class); + private static EventImpl mockEvent() throws InterruptedException { + final RunningHash runningHash = mock(RunningHash.class); + final Hash hash = mock(Hash.class); + final StandardFuture futureHash = mock(StandardFuture.class); + when(futureHash.getAndRethrow()).thenReturn(hash); + when(runningHash.getFutureHash()).thenReturn(futureHash); + final GossipEvent gossipEvent = mock(GossipEvent.class); + final EventImpl outputEvent = mock(EventImpl.class); + when(outputEvent.getRunningHash()).thenReturn(runningHash); + when(outputEvent.getBaseEvent()).thenReturn(gossipEvent); + + return outputEvent; } - /** - * Verify that the consensus handler thread does not make reconnect wait for it to drain the queue of - * consensus rounds. - */ - @RepeatedTest(10) - @Tag(TestQualifierTags.TIME_CONSUMING) - @DisplayName("Reconnect should not wait for queue to be drained") - void queueNotDrainedOnReconnect() { + private static SwirldStateManager mockSwirldStateManager(@NonNull final PlatformState platformState) { + final State consensusState = mock(State.class); + final State stateForSigning = mock(State.class); + when(consensusState.getPlatformState()).thenReturn(platformState); final SwirldStateManager swirldStateManager = mock(SwirldStateManager.class); + when(swirldStateManager.getConsensusState()).thenReturn(consensusState); + when(swirldStateManager.getStateForSigning()).thenReturn(stateForSigning); - // The maximum number of events drained to the QueueThread buffer at a time - final long maxRoundsInBuffer = 100; - final long sleepMillisPerRound = 10; - - // Tracks the number of events handled from the queue - final AtomicInteger numEventsHandled = new AtomicInteger(0); - - // sleep for a little while to pretend to handle an event - doAnswer((e) -> { - Thread.sleep(sleepMillisPerRound); - numEventsHandled.incrementAndGet(); - return null; - }) - .when(swirldStateManager) - .handleConsensusRound(any(ConsensusRound.class)); - - final ExecutorService executor = Executors.newFixedThreadPool(1); - - // Set up a separate thread to invoke clear - final Callable clear = (ThrowingRunnable) () -> consensusRoundHandler.clear(); + return swirldStateManager; + } + @Test + @DisplayName("Normal operation") + void normalOperation() throws InterruptedException { final PlatformContext platformContext = TestPlatformContextBuilder.create().build(); + final PlatformState platformState = mock(PlatformState.class); + final SwirldStateManager swirldStateManager = mockSwirldStateManager(platformState); + + final BlockingQueue stateHashSignQueue = mock(BlockingQueue.class); + final CheckedConsumer waitForEventDurability = mock(CheckedConsumer.class); + final StatusActionSubmitter statusActionSubmitter = mock(StatusActionSubmitter.class); - consensusRoundHandler = new ConsensusRoundHandler( + final AtomicLong roundAppliedToState = new AtomicLong(0); + final Consumer roundAppliedToStateConsumer = roundAppliedToState::set; + + final ConsensusRoundHandler consensusRoundHandler = new ConsensusRoundHandler( platformContext, - getStaticThreadManager(), - selfId, swirldStateManager, - consensusHandlingMetrics, - eventStreamManager, stateHashSignQueue, - e -> {}, - mock(StatusActionSubmitter.class), - (round) -> {}, - new BasicSoftwareVersion(1)); - - final int numRounds = 500; - final ConsensusRound round = mock(ConsensusRound.class); - - // Start the consensus handler and add events to the queue for it to handle - consensusRoundHandler.start(); - for (int i = 0; i < numRounds; i++) { - consensusRoundHandler.consensusRound(round); - } + waitForEventDurability, + statusActionSubmitter, + roundAppliedToStateConsumer, + mock(SoftwareVersion.class)); - // Make the separate thread invoke clear() - final Future future = executor.submit(clear); + final EventImpl keystoneEvent = mockEvent(); + final List events = List.of(mockEvent(), mockEvent(), mockEvent()); - // Wait up to the amount of time it would take for two full buffer of events to be handled. This entire time - // shouldn't be needed, but it's a max time to allow for different systems and other programs running at the - // same time. As long as this is less than the amount of time it takes to handle all the events in the queue, - // the value is valid. - final long maxMillisToWait = Math.round(maxRoundsInBuffer * (sleepMillisPerRound) * 2); + final long consensusRoundNumber = 5L; + final ConsensusRound consensusRound = mockConsensusRound(keystoneEvent, events, consensusRoundNumber); - // Wait for clear() to complete - await().atMost(Duration.of(maxMillisToWait, ChronoUnit.MILLIS)).until(future::isDone); + consensusRoundHandler.handleConsensusRound(consensusRound); - // Verify that no more than 2 buffers worth of events were handled - assertTrue( - numEventsHandled.get() < maxRoundsInBuffer * 2, - "Consensus handler should not enter another doWork() cycle after prepareForReconnect() is called"); - - assertEquals(0, consensusRoundHandler.getRoundsInQueue(), "Consensus queue should be empty"); + for (final EventImpl event : events) { + verify(event).consensusReached(); + } + verify(statusActionSubmitter, never()).submitStatusAction(any(FreezePeriodEnteredAction.class)); + verify(waitForEventDurability).accept(keystoneEvent.getBaseEvent()); + verify(swirldStateManager).handleConsensusRound(consensusRound); + assertEquals(consensusRoundNumber, roundAppliedToState.get()); + verify(swirldStateManager, never()).savedStateInFreezePeriod(); + verify(stateHashSignQueue).put(any(ReservedSignedState.class)); + verify(platformState) + .setRunningEventHash( + events.getLast().getRunningHash().getFutureHash().getAndRethrow()); } - /** - * Tests that consensus events are passed to {@link EventStreamManager#addEvents(List)} exactly once. - */ @Test - void testConsensusEventStream() { - final SwirldState swirldState = new DummySwirldState(); - initConsensusHandler(swirldState); - testEventStream(eventStreamManager, consensusRoundHandler::consensusRound); - } - - /** - * Verifies that {@link EventStreamManager#addEvents(List)} is called the desired number of times. - * - * @param eventStreamManager the instance of {@link EventStreamManager} used by {@link ConsensusRoundHandler} - * @param roundConsumer the round consumer to test - */ - private void testEventStream( - final EventStreamManager eventStreamManager, final Consumer roundConsumer) { - final List events = createEvents(10, 10, true); - final ConsensusRound round = mock(ConsensusRound.class); - when(round.getConsensusEvents()).thenReturn(events); - roundConsumer.accept(round); - verify(eventStreamManager, times(1)).addEvents(events); - } - - private void initConsensusHandler(final SwirldState swirldState) { - final State state = new State(); - state.setSwirldState(swirldState); - - final AddressBook addressBook = new RandomAddressBookGenerator().build(); - + @DisplayName("Round in freeze period") + void freezeHandling() throws InterruptedException { + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); final PlatformState platformState = mock(PlatformState.class); - when(platformState.getClassId()).thenReturn(PlatformState.CLASS_ID); - when(platformState.copy()).thenReturn(platformState); - when(platformState.getAddressBook()).thenReturn(addressBook); + final SwirldStateManager swirldStateManager = mockSwirldStateManager(platformState); + when(swirldStateManager.isInFreezePeriod(any())).thenReturn(true); - state.setPlatformState(platformState); + final BlockingQueue stateHashSignQueue = mock(BlockingQueue.class); + final CheckedConsumer waitForEventDurability = mock(CheckedConsumer.class); + final StatusActionSubmitter statusActionSubmitter = mock(StatusActionSubmitter.class); - final Configuration configuration = new TestConfigBuilder() - .withValue(EventConfig_.MAX_EVENT_QUEUE_FOR_CONS, 500) - .getOrCreateConfig(); - final PlatformContext platformContext = TestPlatformContextBuilder.create() - .withConfiguration(configuration) - .build(); + final AtomicLong roundAppliedToState = new AtomicLong(0); + final Consumer roundAppliedToStateConsumer = roundAppliedToState::set; - final SwirldStateManager swirldStateManager = new SwirldStateManager( + final ConsensusRoundHandler consensusRoundHandler = new ConsensusRoundHandler( platformContext, - addressBook, - selfId, - consensusSystemTransactionManager, - mock(SwirldStateMetrics.class), - mock(StatusActionSubmitter.class), - state, - new BasicSoftwareVersion(1)); - - consensusRoundHandler = new ConsensusRoundHandler( - platformContext, - getStaticThreadManager(), - selfId, swirldStateManager, - consensusHandlingMetrics, - eventStreamManager, stateHashSignQueue, - e -> {}, - mock(StatusActionSubmitter.class), - (round) -> {}, - new BasicSoftwareVersion(1)); - consensusRoundHandler.start(); + waitForEventDurability, + statusActionSubmitter, + roundAppliedToStateConsumer, + mock(SoftwareVersion.class)); + + final EventImpl keystoneEvent = mockEvent(); + final List events = List.of(mockEvent(), mockEvent(), mockEvent()); + + final long consensusRoundNumber = 5L; + final ConsensusRound consensusRound = mockConsensusRound(keystoneEvent, events, consensusRoundNumber); + + consensusRoundHandler.handleConsensusRound(consensusRound); + + for (final EventImpl event : events) { + verify(event, times(1)).consensusReached(); + } + verify(statusActionSubmitter).submitStatusAction(any(FreezePeriodEnteredAction.class)); + verify(waitForEventDurability).accept(keystoneEvent.getBaseEvent()); + verify(swirldStateManager).handleConsensusRound(consensusRound); + assertEquals(consensusRoundNumber, roundAppliedToState.get()); + verify(swirldStateManager).savedStateInFreezePeriod(); + verify(stateHashSignQueue).put(any(ReservedSignedState.class)); + verify(platformState) + .setRunningEventHash( + events.getLast().getRunningHash().getFutureHash().getAndRethrow()); + + final ConsensusRound postFreezeConsensusRound = mockConsensusRound(keystoneEvent, events, consensusRoundNumber); + consensusRoundHandler.handleConsensusRound(postFreezeConsensusRound); + + // these methods were called once from the first round, and shouldn't have been called again from the second + for (final EventImpl event : events) { + verify(event).consensusReached(); + } + verify(statusActionSubmitter).submitStatusAction(any(FreezePeriodEnteredAction.class)); + verify(waitForEventDurability).accept(keystoneEvent.getBaseEvent()); + verify(swirldStateManager).handleConsensusRound(consensusRound); + verify(swirldStateManager).savedStateInFreezePeriod(); + verify(stateHashSignQueue).put(any(ReservedSignedState.class)); + verify(platformState) + .setRunningEventHash( + events.getLast().getRunningHash().getFutureHash().getAndRethrow()); } } 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 d0d5c8a53b34..7dc500ffc2da 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 @@ -81,7 +81,7 @@ public List handlePreconsensusSignatures( public void handlePreconsensusSignatureTransaction( @NonNull final NodeId signerId, @NonNull final StateSignatureTransaction signatureTransaction) { - handlePreconsensusSignatures(List.of(new ScopedSystemTransaction<>(signerId, signatureTransaction))); + handlePreconsensusSignatures(List.of(new ScopedSystemTransaction<>(signerId, null, signatureTransaction))); } @Override @@ -92,7 +92,7 @@ public List handlePostconsensusSignatures( public void handlePostconsensusSignatureTransaction( @NonNull final NodeId signerId, @NonNull final StateSignatureTransaction transaction) { - handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>(signerId, transaction))); + handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>(signerId, null, transaction))); } private List processStates(@Nullable final List states) { 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 401db66dfc47..4e375c51a7ad 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 @@ -21,6 +21,7 @@ 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; import com.swirlds.platform.StateSigner; import com.swirlds.platform.components.LinkedEventIntake; @@ -36,8 +37,10 @@ import com.swirlds.platform.event.preconsensus.PcesWriter; import com.swirlds.platform.event.validation.EventSignatureValidator; import com.swirlds.platform.event.validation.InternalEventValidator; +import com.swirlds.platform.eventhandling.ConsensusRoundHandler; import com.swirlds.platform.gossip.shadowgraph.Shadowgraph; import com.swirlds.platform.state.SwirldStateManager; +import com.swirlds.platform.state.iss.IssDetector; import com.swirlds.platform.state.signed.SignedStateFileManager; import com.swirlds.platform.state.signed.StateSignatureCollector; import org.junit.jupiter.api.DisplayName; @@ -73,7 +76,10 @@ void testBindings() { mock(EventCreationManager.class), mock(SwirldStateManager.class), mock(StateSignatureCollector.class), - mock(FutureEventBuffer.class)); + mock(ConsensusRoundHandler.class), + mock(EventStreamManager.class), + mock(FutureEventBuffer.class), + mock(IssDetector.class)); assertFalse(wiring.getModel().checkForUnboundInputWires()); } diff --git a/platform-sdk/swirlds-platform-core/src/test/resources/com/swirlds/platform/config/legacy/config.txt b/platform-sdk/swirlds-platform-core/src/test/resources/com/swirlds/platform/config/legacy/config.txt index 5c1f3c58990b..31ea825ade2b 100644 --- a/platform-sdk/swirlds-platform-core/src/test/resources/com/swirlds/platform/config/legacy/config.txt +++ b/platform-sdk/swirlds-platform-core/src/test/resources/com/swirlds/platform/config/legacy/config.txt @@ -10,10 +10,9 @@ ############################################################################################### swirld, 123 - app, HashgraphDemo.jar, 1,0,0,0,0,0,0,0,0,0, all # app, HelloSwirldDemo.jar # app, CryptocurrencyDemo.jar -# app, StatsDemo.jar, 1, 3000, 0, 100, -1, 200 + app, StatsDemo.jar, 1, 3000, 0, 100, -1, 200 # ** BEGIN REMOVE FROM SDK RELEASES ** 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 41759e2e87cd..8d73cf5765ca 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 @@ -127,13 +127,7 @@ public TestIntake(@NonNull final AddressBook addressBook, @NonNull final Consens new EventObserverDispatcher(new ShadowGraphEventObserver(shadowGraph), output); final LinkedEventIntake linkedEventIntake = new LinkedEventIntake( - platformContext, - time, - () -> consensus, - dispatcher, - shadowGraph, - intakeEventCounter, - mock(StandardOutputWire.class)); + () -> consensus, dispatcher, shadowGraph, intakeEventCounter, mock(StandardOutputWire.class)); linkedEventIntakeWiring = LinkedEventIntakeWiring.create(schedulers.linkedEventIntakeScheduler()); linkedEventIntakeWiring.bind(linkedEventIntake); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/ConsensusSystemTransactionManagerTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/ConsensusSystemTransactionManagerTests.java deleted file mode 100644 index b653f6a9f208..000000000000 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/components/ConsensusSystemTransactionManagerTests.java +++ /dev/null @@ -1,75 +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.components; - -import static com.swirlds.platform.test.components.TransactionHandlingTestUtils.newDummyRound; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; - -import com.swirlds.common.test.fixtures.DummySystemTransaction; -import com.swirlds.platform.components.transaction.system.ConsensusSystemTransactionHandler; -import com.swirlds.platform.components.transaction.system.ConsensusSystemTransactionManager; -import com.swirlds.platform.state.State; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class ConsensusSystemTransactionManagerTests { - - @Test - @DisplayName("tests that exceptions are handled gracefully") - void testHandleExceptions() { - ConsensusSystemTransactionHandler consumer = - (state, dummySystemTransaction, aLong, version) -> { - throw new IllegalStateException("this is intentionally thrown"); - }; - - final ConsensusSystemTransactionManager manager = new ConsensusSystemTransactionManager(); - manager.addHandler(DummySystemTransaction.class, consumer); - - assertDoesNotThrow(() -> manager.handleRound(mock(State.class), newDummyRound(List.of(1)))); - } - - @Test - @DisplayName("tests handling system transactions") - void testHandle() { - final AtomicInteger handleCount = new AtomicInteger(0); - - ConsensusSystemTransactionHandler consumer = - (state, dummySystemTransaction, aLong, version) -> handleCount.getAndIncrement(); - - final ConsensusSystemTransactionManager manager = new ConsensusSystemTransactionManager(); - manager.addHandler(DummySystemTransaction.class, consumer); - - manager.handleRound(mock(State.class), newDummyRound(List.of(0))); - manager.handleRound(mock(State.class), newDummyRound(List.of(2))); - manager.handleRound(mock(State.class), newDummyRound(List.of(0, 1, 3))); - - assertEquals(6, handleCount.get(), "incorrect number of post-handle calls"); - } - - @Test - @DisplayName("tests handling system transactions, where no handle method has been defined") - void testNoHandleMethod() { - final ConsensusSystemTransactionManager manager = new ConsensusSystemTransactionManager(); - - assertDoesNotThrow( - () -> manager.handleRound(mock(State.class), newDummyRound(List.of(1))), "should not throw "); - } -} diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/ConsensusHashFinderTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/ConsensusHashFinderTests.java index e76d197a24a5..9c04f5233a1d 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/ConsensusHashFinderTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/ConsensusHashFinderTests.java @@ -23,14 +23,12 @@ import static com.swirlds.platform.state.iss.internal.ConsensusHashStatus.UNDECIDED; import static com.swirlds.platform.test.utils.EqualsVerifier.randomHash; 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 static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; import com.swirlds.common.crypto.Hash; import com.swirlds.common.platform.NodeId; -import com.swirlds.platform.dispatch.triggers.flow.StateHashValidityTrigger; +import com.swirlds.platform.metrics.IssMetrics; import com.swirlds.platform.state.iss.internal.ConsensusHashFinder; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; @@ -40,11 +38,13 @@ import java.util.List; import java.util.Random; import java.util.Set; +import java.util.stream.Collectors; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; @DisplayName("ConsensusHashFinder Tests") class ConsensusHashFinderTests { @@ -131,11 +131,7 @@ void singlePartitionTest(final long totalWeight) { final long standardDeviationWeight = totalWeight / 200; final Hash hash = randomHash(random); - final StateHashValidityTrigger stateHashValidityDispatcher = - (final Long round, final NodeId nodeId, final Hash nodeHash, final Hash consensusHash) -> - assertEquals(nodeHash, consensusHash, "no disagreements expected"); - - final ConsensusHashFinder hashFinder = new ConsensusHashFinder(stateHashValidityDispatcher, 0, totalWeight); + final ConsensusHashFinder hashFinder = new ConsensusHashFinder(0, totalWeight, Mockito.mock(IssMetrics.class)); assertEquals(totalWeight, hashFinder.getTotalWeight(), "unexpected total weight"); assertEquals(0, hashFinder.getHashReportedWeight(), "no weight should be accumulated yet"); assertEquals(0, hashFinder.getPartitionMap().size(), "there shouldn't be any partitions yet"); @@ -185,20 +181,9 @@ void singleBarelyValidPartitionTest(final long totalWeight) { final long standardDeviationWeight = totalWeight / 200; final Hash expectedConsensusHash = randomHash(random); - final Set disagreeingNodes = new HashSet<>(); final Set expectedDisagreeingNodes = new HashSet<>(); - final StateHashValidityTrigger stateHashValidityDispatcher = - (final Long round, final NodeId nodeId, final Hash nodeHash, final Hash consensusHash) -> { - assertEquals(consensusHash, expectedConsensusHash, "unexpected consensus hash"); - - if (!nodeHash.equals(consensusHash)) { - assertNotEquals(consensusHash, nodeHash, "hash should disagree"); - assertTrue( - disagreeingNodes.add(nodeId), "should not be called multiple times on the same node"); - } - }; - final ConsensusHashFinder hashFinder = new ConsensusHashFinder(stateHashValidityDispatcher, 0, totalWeight); + final ConsensusHashFinder hashFinder = new ConsensusHashFinder(0, totalWeight, Mockito.mock(IssMetrics.class)); long remainingWeight = totalWeight; @@ -229,7 +214,13 @@ void singleBarelyValidPartitionTest(final long totalWeight) { assertEquals(DECIDED, hashFinder.getStatus(), "should be decided by now"); assertEquals(expectedConsensusHash, hashFinder.getConsensusHash(), "incorrect hash chosen"); - assertEquals(expectedDisagreeingNodes, disagreeingNodes, "disagreeing node set incorrect"); + assertEquals( + expectedDisagreeingNodes, + hashFinder.getPartitionMap().entrySet().stream() + .filter(entry -> !entry.getKey().equals(expectedConsensusHash)) + .flatMap(entry -> entry.getValue().getNodes().stream()) + .collect(Collectors.toSet()), + "disagreeing node set incorrect"); } @ParameterizedTest @@ -240,11 +231,7 @@ void almostCompletePartitionTest(final long totalWeight) { final long averageWeight = totalWeight / 100; final long standardDeviationWeight = totalWeight / 200; - final StateHashValidityTrigger stateHashValidityDispatcher = - (final Long round, final NodeId nodeId, final Hash nodeHash, final Hash consensusHash) -> - fail("no disagreement expected"); - - final ConsensusHashFinder hashFinder = new ConsensusHashFinder(stateHashValidityDispatcher, 0, totalWeight); + final ConsensusHashFinder hashFinder = new ConsensusHashFinder(0, totalWeight, Mockito.mock(IssMetrics.class)); long remainingWeight = totalWeight; @@ -284,11 +271,7 @@ void lotsOfSmallPartitionsTest(final long totalWeight) { final long averageWeight = totalWeight / 100; final long standardDeviationWeight = totalWeight / 200; - final StateHashValidityTrigger stateHashValidityDispatcher = - (final Long round, final NodeId nodeId, final Hash nodeHash, final Hash consensusHash) -> - fail("no disagreement expected"); - - final ConsensusHashFinder hashFinder = new ConsensusHashFinder(stateHashValidityDispatcher, 0, totalWeight); + final ConsensusHashFinder hashFinder = new ConsensusHashFinder(0, totalWeight, Mockito.mock(IssMetrics.class)); long remainingWeight = totalWeight; @@ -322,11 +305,7 @@ void earlyIssDetectionTest(final long totalWeight) { final long averageWeight = totalWeight / 100; final long standardDeviationWeight = totalWeight / 200; - final StateHashValidityTrigger stateHashValidityDispatcher = - (final Long round, final NodeId nodeId, final Hash nodeHash, final Hash consensusHash) -> - fail("no consensus hash should be found"); - - final ConsensusHashFinder hashFinder = new ConsensusHashFinder(stateHashValidityDispatcher, 0, totalWeight); + final ConsensusHashFinder hashFinder = new ConsensusHashFinder(0, totalWeight, Mockito.mock(IssMetrics.class)); long remainingWeight = totalWeight; @@ -375,20 +354,9 @@ void completePartitionIsLastTest(final long totalWeight) { final long standardDeviationWeight = totalWeight / 200; final Hash expectedConsensusHash = randomHash(random); - final Set disagreeingNodes = new HashSet<>(); final Set expectedDisagreeingNodes = new HashSet<>(); - final StateHashValidityTrigger stateHashValidityDispatcher = - (final Long round, final NodeId nodeId, final Hash nodeHash, final Hash consensusHash) -> { - assertEquals(consensusHash, expectedConsensusHash, "unexpected consensus hash"); - - if (!nodeHash.equals(consensusHash)) { - assertNotEquals(consensusHash, nodeHash, "hash should disagree"); - assertTrue( - disagreeingNodes.add(nodeId), "should not be called multiple times on the same node"); - } - }; - final ConsensusHashFinder hashFinder = new ConsensusHashFinder(stateHashValidityDispatcher, 0, totalWeight); + final ConsensusHashFinder hashFinder = new ConsensusHashFinder(0, totalWeight, Mockito.mock(IssMetrics.class)); long remainingWeight = totalWeight; @@ -429,6 +397,13 @@ void completePartitionIsLastTest(final long totalWeight) { assertEquals(DECIDED, hashFinder.getStatus(), "should be decided by now"); assertEquals(expectedConsensusHash, hashFinder.getConsensusHash(), "incorrect hash chosen"); - assertEquals(expectedDisagreeingNodes, disagreeingNodes, "disagreeing node set incorrect"); + assertEquals( + expectedDisagreeingNodes, + hashFinder.getPartitionMap().entrySet().stream() + .filter(entry -> !entry.getKey().equals(expectedConsensusHash)) + .flatMap(entry -> entry.getValue().getNodes().stream()) + .collect(Collectors.toSet()), + "disagreeing node set incorrect"); + assertTrue(hashFinder.hasDisagreement(), "should have a disagreement"); } } 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 new file mode 100644 index 000000000000..3b3834dc49ec --- /dev/null +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssDetectorTestHelper.java @@ -0,0 +1,86 @@ +/* + * 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.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 edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public class IssDetectorTestHelper extends IssDetector { + /** the default epoch hash to use */ + private static final Hash DEFAULT_EPOCH_HASH = null; + + private final List issList = 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); + } + + @Override + public List roundCompleted(final long round) { + return processList(super.roundCompleted(round)); + } + + @Override + public List handlePostconsensusSignatures( + @NonNull final List> transactions) { + return processList(super.handlePostconsensusSignatures(transactions)); + } + + @Override + public List newStateHashed(@NonNull final ReservedSignedState state) { + return processList(super.newStateHashed(state)); + } + + @Override + public List overridingState(@NonNull final ReservedSignedState state) { + return processList(super.overridingState(state)); + } + + public List getIssList() { + return issList; + } + + public int getIssCount() { + return issList.size(); + } + + public long getIssCount(final IssType... types) { + return issList.stream() + .map(IssNotification::getIssType) + .filter(Set.of(types)::contains) + .count(); + } + + private List processList(final List list) { + Optional.ofNullable(list).ifPresent(issList::addAll); + return list; + } +} diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/ConsensusHashManagerTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssDetectorTests.java similarity index 61% rename from platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/ConsensusHashManagerTests.java rename to platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssDetectorTests.java index 201288a62351..c1a01bcc2534 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/ConsensusHashManagerTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssDetectorTests.java @@ -20,35 +20,32 @@ import static com.swirlds.common.test.fixtures.RandomUtils.randomHash; import static com.swirlds.common.utility.Threshold.MAJORITY; import static com.swirlds.common.utility.Threshold.SUPER_MAJORITY; -import static com.swirlds.platform.state.iss.ConsensusHashManager.DO_NOT_IGNORE_ROUNDS; -import static com.swirlds.platform.test.DispatchBuilderUtils.getDefaultDispatchConfiguration; +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.assertFalse; 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.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.mock; -import com.swirlds.base.time.Time; 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.RandomAddressBookGenerator; 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.dispatch.DispatchBuilder; -import com.swirlds.platform.dispatch.triggers.error.CatastrophicIssTrigger; -import com.swirlds.platform.dispatch.triggers.error.SelfIssTrigger; -import com.swirlds.platform.state.iss.ConsensusHashManager; +import com.swirlds.platform.state.State; 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.transaction.StateSignatureTransaction; import java.util.ArrayList; import java.util.HashSet; @@ -56,15 +53,13 @@ import java.util.List; import java.util.Random; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.StreamSupport; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; @DisplayName("ConsensusHashManager Tests") -class ConsensusHashManagerTests { - /** the default epoch hash to use */ - private static final Hash DEFAULT_EPOCH_HASH = null; +class IssDetectorTests { @Test @DisplayName("Valid Signatures After Hash Test") @@ -80,43 +75,28 @@ void validSignaturesAfterHashTest() { final PlatformContext platformContext = TestPlatformContextBuilder.create().build(); - final DispatchBuilder dispatchBuilder = new DispatchBuilder(getDefaultDispatchConfiguration()); - final ConsensusHashManager manager = new ConsensusHashManager( - platformContext, - Time.getCurrent(), - dispatchBuilder, - addressBook, - DEFAULT_EPOCH_HASH, - new BasicSoftwareVersion(1), - false, - DO_NOT_IGNORE_ROUNDS); - - final AtomicBoolean fail = new AtomicBoolean(false); - dispatchBuilder.registerObserver(this, SelfIssTrigger.class, (a, b, c) -> fail.set(true)); - dispatchBuilder.registerObserver(this, CatastrophicIssTrigger.class, (a, b) -> fail.set(true)); - - dispatchBuilder.start(); + final IssDetectorTestHelper manager = + new IssDetectorTestHelper(platformContext, addressBook, DO_NOT_IGNORE_ROUNDS); final int rounds = 1_000; for (long round = 1; round <= rounds; round++) { final Hash roundHash = randomHash(random); if (round == 1) { - manager.overridingStateObserver(round, roundHash); + manager.overridingState(mockState(round, roundHash)); } else { manager.roundCompleted(round); - manager.stateHashedObserver(round, roundHash); - } - - for (final Address address : addressBook) { - manager.handlePostconsensusSignatureTransaction( - address.getNodeId(), - new StateSignatureTransaction(round, mock(Signature.class), roundHash), - new BasicSoftwareVersion(1)); + 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))); } - - assertFalse(fail.get(), "failure condition triggered"); + assertTrue(manager.getIssList().isEmpty(), "there should be no ISS notifications"); } @Test @@ -146,14 +126,12 @@ void mixedOrderTest() { int expectedSelfIssCount = 0; int expectedCatastrophicIssCount = 0; final List selfHashes = new ArrayList<>(roundsNonAncient); - final List consensusHashes = new ArrayList<>(roundsNonAncient); for (int round = 0; round < roundsNonAncient; round++) { final RoundHashValidatorTests.HashGenerationData data; if (random.nextDouble() < 2.0 / 3) { // Choose hashes so that there is a valid consensus hash data = generateRegularNodeHashes(random, addressBook, round); - consensusHashes.add(data.consensusHash()); HashValidityStatus expectedStatus = null; @@ -179,7 +157,6 @@ void mixedOrderTest() { // Choose hashes that will result in a catastrophic ISS data = generateCatastrophicNodeHashes(random, addressBook, round); roundData.add(data); - consensusHashes.add(null); expectedRoundStatus.add(HashValidityStatus.CATASTROPHIC_ISS); expectedCatastrophicIssCount++; } @@ -194,70 +171,10 @@ void mixedOrderTest() { } } - final DispatchBuilder dispatchBuilder = new DispatchBuilder(getDefaultDispatchConfiguration()); - final ConsensusHashManager manager = new ConsensusHashManager( - platformContext, - Time.getCurrent(), - dispatchBuilder, - addressBook, - DEFAULT_EPOCH_HASH, - new BasicSoftwareVersion(1), - false, - DO_NOT_IGNORE_ROUNDS); - - final AtomicBoolean fail = new AtomicBoolean(false); - final AtomicInteger issCount = new AtomicInteger(0); - final AtomicInteger catastrophicIssCount = new AtomicInteger(0); - final Set observedRounds = new HashSet<>(); - - dispatchBuilder.registerObserver( - this, SelfIssTrigger.class, (final Long round, final Hash selfStateHash, final Hash consensusHash) -> { - try { - - assertTrue(observedRounds.add(round), "rounds should trigger a notification at most once"); - - final int roundIndex = (int) (long) round; - final HashValidityStatus expectedStatus = expectedRoundStatus.get(roundIndex); - - assertEquals(selfHashes.get(roundIndex), selfStateHash, "invalid self hash"); - - if (expectedStatus == HashValidityStatus.SELF_ISS) { - assertEquals(consensusHashes.get(roundIndex), consensusHash, "unexpected consensus hash"); - issCount.getAndIncrement(); - } else { - fail("invalid status " + expectedStatus); - } - } catch (final Throwable t) { - t.printStackTrace(); - fail.set(true); - } - }); - - dispatchBuilder.registerObserver( - this, CatastrophicIssTrigger.class, (final Long round, final Hash selfStateHash) -> { - try { - final int roundIndex = (int) (long) round; - - assertTrue(observedRounds.add(round), "rounds should trigger a notification at most once"); - - final HashValidityStatus expectedStatus = expectedRoundStatus.get(roundIndex); - - assertEquals(selfHashes.get(roundIndex), selfStateHash, "invalid self hash"); - - if (expectedStatus == HashValidityStatus.CATASTROPHIC_ISS) { - catastrophicIssCount.getAndIncrement(); - } else { - fail("invalid status " + expectedStatus); - } - } catch (final Throwable t) { - t.printStackTrace(); - fail.set(true); - } - }); - - dispatchBuilder.start(); + final IssDetectorTestHelper manager = + new IssDetectorTestHelper(platformContext, addressBook, DO_NOT_IGNORE_ROUNDS); - manager.overridingStateObserver(0L, selfHashes.get(0)); + manager.overridingState(mockState(0L, selfHashes.getFirst())); // Start collecting data for rounds. for (long round = 1; round < roundsNonAncient; round++) { @@ -266,14 +183,14 @@ void mixedOrderTest() { // Add all the self hashes. for (long round = 1; round < roundsNonAncient; round++) { - manager.stateHashedObserver(round, selfHashes.get((int) round)); + manager.newStateHashed(mockState(round, selfHashes.get((int) round))); } // 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().remove(0)); + operations.add(roundData.get(index).nodeList().removeFirst()); if (roundData.get(index).nodeList().isEmpty()) { roundData.remove(index); } @@ -281,27 +198,50 @@ void mixedOrderTest() { assertEquals(roundsNonAncient * addressBook.getSize(), operations.size(), "unexpected number of operations"); - for (final RoundHashValidatorTests.NodeHashInfo nodeHashInfo : operations) { - final NodeId nodeId = nodeHashInfo.nodeId(); - - manager.handlePostconsensusSignatureTransaction( - nodeId, - new StateSignatureTransaction( - nodeHashInfo.round(), mock(Signature.class), nodeHashInfo.nodeStateHash()), - new BasicSoftwareVersion(1)); - } + 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))); // Shifting after completion should have no side effects for (long i = roundsNonAncient; i < 2L * roundsNonAncient - 1; i++) { manager.roundCompleted(i); } - assertFalse(fail.get(), "exception thrown in ISS callback"); - assertEquals(expectedSelfIssCount, issCount.get(), "unexpected number of ISS callbacks"); + assertEquals( + expectedSelfIssCount, + manager.getIssList().stream() + .filter(n -> n.getIssType() == IssType.SELF_ISS) + .count(), + "unexpected number of ISS callbacks"); assertEquals( expectedCatastrophicIssCount, - catastrophicIssCount.get(), + 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; + // if there was an other-ISS, then the round should still be valid + case VALID -> IssType.OTHER_ISS; + default -> throw new IllegalStateException( + "Unexpected value: " + expectedRoundStatus.get((int) n.getRound())); + }; + assertEquals( + expectedType, + n.getIssType(), + "Expected status for round %d to be %s but was %s" + .formatted(n.getRound(), expectedRoundStatus.get((int) n.getRound()), n.getIssType())); + }); + final Set observedRounds = new HashSet<>(); + manager.getIssList() + .forEach(n -> assertTrue( + observedRounds.add(n.getRound()), "rounds should trigger a notification at most once")); } /** @@ -351,24 +291,8 @@ void earlyAddTest() { .build(); final NodeId selfId = addressBook.getNodeId(0); - final DispatchBuilder dispatchBuilder = new DispatchBuilder(getDefaultDispatchConfiguration()); - final ConsensusHashManager manager = new ConsensusHashManager( - platformContext, - Time.getCurrent(), - dispatchBuilder, - addressBook, - DEFAULT_EPOCH_HASH, - new BasicSoftwareVersion(1), - false, - DO_NOT_IGNORE_ROUNDS); - - dispatchBuilder.registerObserver( - this, CatastrophicIssTrigger.class, (a, b) -> fail("did not expect catastrophic ISS")); - - final AtomicInteger issCount = new AtomicInteger(); - dispatchBuilder.registerObserver(this, SelfIssTrigger.class, (a, b, c) -> issCount.getAndIncrement()); - - dispatchBuilder.start(); + final IssDetectorTestHelper manager = + new IssDetectorTestHelper(platformContext, addressBook, DO_NOT_IGNORE_ROUNDS); // Start collecting data for rounds. for (long round = 0; round < roundsNonAncient; round++) { @@ -385,16 +309,16 @@ void earlyAddTest() { if (info.nodeId() == selfId) { assertThrows( IllegalStateException.class, - () -> manager.stateHashedObserver(targetRound, info.nodeStateHash()), + () -> manager.newStateHashed(mockState(targetRound, info.nodeStateHash())), "should not be able to add hash for round not being tracked"); } - manager.handlePostconsensusSignatureTransaction( + manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( info.nodeId(), - new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash()), - new BasicSoftwareVersion(1)); + new BasicSoftwareVersion(1), + new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash())))); } - assertEquals(0, issCount.get(), "all data should have been ignored"); + 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. @@ -403,15 +327,15 @@ void earlyAddTest() { manager.roundCompleted(targetRound); for (final RoundHashValidatorTests.NodeHashInfo info : data.nodeList()) { if (info.nodeId() == selfId) { - manager.stateHashedObserver(targetRound, info.nodeStateHash()); + manager.newStateHashed(mockState(targetRound, info.nodeStateHash())); } - manager.handlePostconsensusSignatureTransaction( + manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( info.nodeId(), - new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash()), - new BasicSoftwareVersion(1)); + new BasicSoftwareVersion(1), + new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash())))); } - assertEquals(1, issCount.get(), "data should not have been ignored"); + assertEquals(1, manager.getIssList().size(), "data should not have been ignored"); } @Test @@ -433,24 +357,8 @@ void lateAddTest() { .build(); final NodeId selfId = addressBook.getNodeId(0); - final DispatchBuilder dispatchBuilder = new DispatchBuilder(getDefaultDispatchConfiguration()); - final ConsensusHashManager manager = new ConsensusHashManager( - platformContext, - Time.getCurrent(), - dispatchBuilder, - addressBook, - DEFAULT_EPOCH_HASH, - new BasicSoftwareVersion(1), - false, - DO_NOT_IGNORE_ROUNDS); - - dispatchBuilder.registerObserver( - this, CatastrophicIssTrigger.class, (a, b) -> fail("did not expect catastrophic ISS")); - - final AtomicInteger issCount = new AtomicInteger(); - dispatchBuilder.registerObserver(this, SelfIssTrigger.class, (a, b, c) -> issCount.getAndIncrement()); - - dispatchBuilder.start(); + 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. @@ -467,16 +375,16 @@ void lateAddTest() { if (info.nodeId() == selfId) { assertThrows( IllegalStateException.class, - () -> manager.stateHashedObserver(targetRound, info.nodeStateHash()), + () -> manager.newStateHashed(mockState(targetRound, info.nodeStateHash())), "should not be able to add hash for round not being tracked"); } - manager.handlePostconsensusSignatureTransaction( + manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( info.nodeId(), - new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash()), - new BasicSoftwareVersion(1)); + new BasicSoftwareVersion(1), + new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash())))); } - assertEquals(0, issCount.get(), "all data should have been ignored"); + assertEquals(0, manager.getIssCount(), "all data should have been ignored"); } @Test @@ -498,23 +406,8 @@ void shiftBeforeCompleteTest() { .build(); final NodeId selfId = addressBook.getNodeId(0); - final DispatchBuilder dispatchBuilder = new DispatchBuilder(getDefaultDispatchConfiguration()); - final ConsensusHashManager manager = new ConsensusHashManager( - platformContext, - Time.getCurrent(), - dispatchBuilder, - addressBook, - DEFAULT_EPOCH_HASH, - new BasicSoftwareVersion(1), - false, - DO_NOT_IGNORE_ROUNDS); - - final AtomicInteger issCount = new AtomicInteger(); - dispatchBuilder.registerObserver(this, CatastrophicIssTrigger.class, (a, b) -> issCount.getAndIncrement()); - - dispatchBuilder.registerObserver(this, SelfIssTrigger.class, (a, b, c) -> issCount.getAndIncrement()); - - dispatchBuilder.start(); + final IssDetectorTestHelper manager = + new IssDetectorTestHelper(platformContext, addressBook, DO_NOT_IGNORE_ROUNDS); // Start collecting data for rounds. for (long round = 0; round < roundsNonAncient; round++) { @@ -529,7 +422,7 @@ void shiftBeforeCompleteTest() { for (final RoundHashValidatorTests.NodeHashInfo info : data.nodeList()) { if (info.nodeId() == selfId) { - manager.stateHashedObserver(0L, info.nodeStateHash()); + manager.newStateHashed(mockState(0L, info.nodeStateHash())); } } @@ -542,16 +435,18 @@ void shiftBeforeCompleteTest() { } submittedWeight += weight; - manager.handlePostconsensusSignatureTransaction( + manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( info.nodeId(), - new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash()), - new BasicSoftwareVersion(1)); + new BasicSoftwareVersion(1), + new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash())))); } // Shift the window even though we have not added enough data for a decision - manager.roundCompleted((long) roundsNonAncient); + manager.roundCompleted(roundsNonAncient); + + System.out.println(manager.getIssList()); - assertEquals(0, issCount.get(), "there wasn't enough data submitted to observe the ISS"); + assertEquals(0, manager.getIssCount(), "there wasn't enough data submitted to observe the ISS"); } /** @@ -603,23 +498,8 @@ void catastrophicShiftBeforeCompleteTest() { .build(); final NodeId selfId = addressBook.getNodeId(0); - final DispatchBuilder dispatchBuilder = new DispatchBuilder(getDefaultDispatchConfiguration()); - final ConsensusHashManager manager = new ConsensusHashManager( - platformContext, - Time.getCurrent(), - dispatchBuilder, - addressBook, - DEFAULT_EPOCH_HASH, - new BasicSoftwareVersion(1), - false, - DO_NOT_IGNORE_ROUNDS); - - final AtomicInteger issCount = new AtomicInteger(); - dispatchBuilder.registerObserver(this, CatastrophicIssTrigger.class, (a, b) -> issCount.getAndIncrement()); - - dispatchBuilder.registerObserver(this, SelfIssTrigger.class, (a, b, c) -> fail("did not expect self ISS")); - - dispatchBuilder.start(); + final IssDetectorTestHelper manager = + new IssDetectorTestHelper(platformContext, addressBook, DO_NOT_IGNORE_ROUNDS); // Start collecting data for rounds. for (long round = 0; round < roundsNonAncient; round++) { @@ -634,7 +514,7 @@ void catastrophicShiftBeforeCompleteTest() { for (final RoundHashValidatorTests.NodeHashInfo info : data) { if (info.nodeId() == selfId) { - manager.stateHashedObserver(0L, info.nodeStateHash()); + manager.newStateHashed(mockState(0L, info.nodeStateHash())); } } @@ -642,10 +522,10 @@ void catastrophicShiftBeforeCompleteTest() { for (final RoundHashValidatorTests.NodeHashInfo info : data) { final long weight = addressBook.getAddress(info.nodeId()).getWeight(); - manager.handlePostconsensusSignatureTransaction( + manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( info.nodeId(), - new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash()), - new BasicSoftwareVersion(1)); + new BasicSoftwareVersion(1), + new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash())))); // Stop once we have added >2/3. We should not have decided yet, but will // have gathered enough to declare a catastrophic ISS @@ -657,9 +537,9 @@ 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((long) roundsNonAncient); + manager.roundCompleted(roundsNonAncient); - assertEquals(1, issCount.get(), "shifting should have caused an ISS"); + assertEquals(1, manager.getIssCount(), "shifting should have caused an ISS"); } @Test @@ -681,23 +561,8 @@ void bigShiftTest() { .build(); final NodeId selfId = addressBook.getNodeId(0); - final DispatchBuilder dispatchBuilder = new DispatchBuilder(getDefaultDispatchConfiguration()); - final ConsensusHashManager manager = new ConsensusHashManager( - platformContext, - Time.getCurrent(), - dispatchBuilder, - addressBook, - DEFAULT_EPOCH_HASH, - new BasicSoftwareVersion(1), - false, - DO_NOT_IGNORE_ROUNDS); - - final AtomicInteger issCount = new AtomicInteger(); - dispatchBuilder.registerObserver(this, CatastrophicIssTrigger.class, (a, b) -> issCount.getAndIncrement()); - - dispatchBuilder.registerObserver(this, SelfIssTrigger.class, (a, b, c) -> fail("did not expect self ISS")); - - dispatchBuilder.start(); + final IssDetectorTestHelper manager = + new IssDetectorTestHelper(platformContext, addressBook, DO_NOT_IGNORE_ROUNDS); // Start collecting data for rounds. for (long round = 0; round < roundsNonAncient; round++) { @@ -712,7 +577,7 @@ void bigShiftTest() { for (final RoundHashValidatorTests.NodeHashInfo info : data) { if (info.nodeId() == selfId) { - manager.stateHashedObserver(0L, info.nodeStateHash()); + manager.newStateHashed(mockState(0L, info.nodeStateHash())); } } @@ -720,10 +585,10 @@ void bigShiftTest() { for (final RoundHashValidatorTests.NodeHashInfo info : data) { final long weight = addressBook.getAddress(info.nodeId()).getWeight(); - manager.handlePostconsensusSignatureTransaction( + manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( info.nodeId(), - new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash()), - new BasicSoftwareVersion(1)); + new BasicSoftwareVersion(1), + new StateSignatureTransaction(targetRound, mock(Signature.class), info.nodeStateHash())))); // Stop once we have added >2/3. We should not have decided yet, but will // have gathered enough to declare a catastrophic ISS @@ -734,9 +599,9 @@ void bigShiftTest() { } // Shifting the window a great distance should not trigger the ISS. - manager.overridingStateObserver(roundsNonAncient + 100L, randomHash(random)); + manager.overridingState(mockState(roundsNonAncient + 100L, randomHash(random))); - assertEquals(0, issCount.get(), "there wasn't enough data submitted to observe the ISS"); + assertEquals(0, manager.getIssCount(), "there wasn't enough data submitted to observe the ISS"); } @Test @@ -753,50 +618,45 @@ void ignoredRoundTest() { final PlatformContext platformContext = TestPlatformContextBuilder.create().build(); - final DispatchBuilder dispatchBuilder = new DispatchBuilder(getDefaultDispatchConfiguration()); - final ConsensusHashManager manager = new ConsensusHashManager( - platformContext, - Time.getCurrent(), - dispatchBuilder, - addressBook, - DEFAULT_EPOCH_HASH, - new BasicSoftwareVersion(1), - false, - 1); - - final AtomicBoolean fail = new AtomicBoolean(false); - dispatchBuilder.registerObserver(this, SelfIssTrigger.class, (a, b, c) -> fail.set(true)); - dispatchBuilder.registerObserver(this, CatastrophicIssTrigger.class, (a, b) -> fail.set(true)); - - dispatchBuilder.start(); + final IssDetectorTestHelper manager = new IssDetectorTestHelper(platformContext, addressBook, 1); final int rounds = 1_000; for (long round = 1; round <= rounds; round++) { final Hash roundHash = randomHash(random); if (round == 1) { - manager.overridingStateObserver(round, roundHash); + manager.overridingState(mockState(round, roundHash)); } else { manager.roundCompleted(round); - manager.stateHashedObserver(round, roundHash); + manager.newStateHashed(mockState(round, roundHash)); } for (final Address address : addressBook) { if (round == 1) { // Intentionally send bad hashes in the first round. We are configured to ignore this round. - manager.handlePostconsensusSignatureTransaction( + manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( address.getNodeId(), - new StateSignatureTransaction(round, mock(Signature.class), randomHash(random)), - new BasicSoftwareVersion(1)); + new BasicSoftwareVersion(1), + new StateSignatureTransaction(round, mock(Signature.class), randomHash(random))))); } else { - manager.handlePostconsensusSignatureTransaction( + manager.handlePostconsensusSignatures(List.of(new ScopedSystemTransaction<>( address.getNodeId(), - new StateSignatureTransaction(round, mock(Signature.class), roundHash), - new BasicSoftwareVersion(1)); + new BasicSoftwareVersion(1), + new StateSignatureTransaction(round, mock(Signature.class), roundHash)))); } } } + assertEquals(0, manager.getIssCount(), "ISS should have been ignored"); + } - assertFalse(fail.get(), "failure condition triggered"); + 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); + return rs; } } 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 0a2549ab04bc..17b693ce3f8a 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 @@ -16,25 +16,21 @@ package com.swirlds.platform.test.state; -import static com.swirlds.common.test.fixtures.RandomUtils.randomHash; 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.mock; -import com.swirlds.common.crypto.Hash; import com.swirlds.common.merkle.utility.SerializableLong; -import com.swirlds.common.platform.NodeId; import com.swirlds.common.scratchpad.Scratchpad; import com.swirlds.config.api.Configuration; import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; import com.swirlds.platform.components.common.output.FatalErrorConsumer; -import com.swirlds.platform.components.state.output.IssConsumer; 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.status.StatusActionSubmitter; +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 org.junit.jupiter.api.DisplayName; @@ -42,47 +38,12 @@ @DisplayName("IssHandler Tests") class IssHandlerTests { - - @Test - @DisplayName("Hash Disagreement From Self") - void hashDisagreementFromSelf() { - final Configuration configuration = - new TestConfigBuilder().withValue("state.haltOnAnyIss", true).getOrCreateConfig(); - - final StateConfig stateConfig = configuration.getConfigData(StateConfig.class); - final long selfId = 0; - - final AtomicInteger freezeCount = new AtomicInteger(); - final AtomicInteger shutdownCount = new AtomicInteger(); - - final HaltRequestedConsumer haltRequestedConsumer = (final String reason) -> freezeCount.getAndIncrement(); - - final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); - - final Scratchpad simpleScratchpad = new SimpleScratchpad<>(); - final IssHandler handler = new IssHandler( - stateConfig, - new NodeId(selfId), - mock(StatusActionSubmitter.class), - haltRequestedConsumer, - fatalErrorConsumer, - (r, type, otherId) -> {}, - simpleScratchpad); - - handler.stateHashValidityObserver(1234L, new NodeId(selfId), randomHash(), randomHash()); - - assertEquals(0, freezeCount.get(), "unexpected freeze count"); - assertEquals(0, shutdownCount.get(), "unexpected shutdown count"); - assertNull(simpleScratchpad.get(IssScratchpad.LAST_ISS_ROUND)); - } - @Test - @DisplayName("Hash Disagreement Always Freeze") - void hashDisagreementAlwaysFreeze() { + @DisplayName("Other ISS Always Freeze") + void otherIssAlwaysFreeze() { final Configuration configuration = new TestConfigBuilder().withValue("state.haltOnAnyIss", true).getOrCreateConfig(); final StateConfig stateConfig = configuration.getConfigData(StateConfig.class); - final long selfId = 0; final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); @@ -92,22 +53,16 @@ void hashDisagreementAlwaysFreeze() { final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); final Scratchpad simpleScratchpad = new SimpleScratchpad<>(); - final IssHandler handler = new IssHandler( - stateConfig, - new NodeId(selfId), - mock(StatusActionSubmitter.class), - haltRequestedConsumer, - fatalErrorConsumer, - (r, type, otherId) -> {}, - simpleScratchpad); + final IssHandler handler = + new IssHandler(stateConfig, haltRequestedConsumer, fatalErrorConsumer, simpleScratchpad); - handler.stateHashValidityObserver(1234L, new NodeId(selfId + 1), randomHash(), randomHash()); + handler.issObserved(new IssNotification(1234L, IssType.OTHER_ISS)); assertEquals(1, freezeCount.get(), "unexpected freeze count"); assertEquals(0, shutdownCount.get(), "unexpected shutdown count"); // Once frozen, this should become a no-op - handler.stateHashValidityObserver(1234L, new NodeId(selfId + 1), randomHash(), randomHash()); + handler.issObserved(new IssNotification(1234L, IssType.OTHER_ISS)); assertEquals(1, freezeCount.get(), "unexpected freeze count"); assertEquals(0, shutdownCount.get(), "unexpected shutdown count"); @@ -117,12 +72,11 @@ void hashDisagreementAlwaysFreeze() { } @Test - @DisplayName("Hash Disagreement No Action") - void hashDisagreementNoAction() { + @DisplayName("Other ISS No Action") + void otherIssNoAction() { final Configuration configuration = new TestConfigBuilder().withValue("state.haltOnAnyIss", false).getOrCreateConfig(); final StateConfig stateConfig = configuration.getConfigData(StateConfig.class); - final long selfId = 0; final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); @@ -132,16 +86,10 @@ void hashDisagreementNoAction() { final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); final Scratchpad simpleScratchpad = new SimpleScratchpad<>(); - final IssHandler handler = new IssHandler( - stateConfig, - new NodeId(selfId), - mock(StatusActionSubmitter.class), - haltRequestedConsumer, - fatalErrorConsumer, - (r, type, otherId) -> {}, - simpleScratchpad); + final IssHandler handler = + new IssHandler(stateConfig, haltRequestedConsumer, fatalErrorConsumer, simpleScratchpad); - handler.stateHashValidityObserver(1234L, new NodeId(selfId + 1), randomHash(), randomHash()); + handler.issObserved(new IssNotification(1234L, IssType.OTHER_ISS)); assertEquals(0, freezeCount.get(), "unexpected freeze count"); assertEquals(0, shutdownCount.get(), "unexpected shutdown count"); @@ -157,7 +105,6 @@ void selfIssAutomatedRecovery() { .withValue("state.automatedSelfIssRecovery", true) .getOrCreateConfig(); final StateConfig stateConfig = configuration.getConfigData(StateConfig.class); - final long selfId = 0; final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); @@ -167,16 +114,10 @@ void selfIssAutomatedRecovery() { final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); final Scratchpad simpleScratchpad = new SimpleScratchpad<>(); - final IssHandler handler = new IssHandler( - stateConfig, - new NodeId(selfId), - mock(StatusActionSubmitter.class), - haltRequestedConsumer, - fatalErrorConsumer, - (r, type, otherId) -> {}, - simpleScratchpad); + final IssHandler handler = + new IssHandler(stateConfig, haltRequestedConsumer, fatalErrorConsumer, simpleScratchpad); - handler.selfIssObserver(1234L, randomHash(), randomHash()); + handler.issObserved(new IssNotification(1234L, IssType.SELF_ISS)); assertEquals(0, freezeCount.get(), "unexpected freeze count"); assertEquals(1, shutdownCount.get(), "unexpected shutdown count"); @@ -194,7 +135,6 @@ void selfIssNoAction() { .withValue("state.automatedSelfIssRecovery", false) .getOrCreateConfig(); final StateConfig stateConfig = configuration.getConfigData(StateConfig.class); - final long selfId = 0; final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); @@ -204,16 +144,10 @@ void selfIssNoAction() { final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); final Scratchpad simpleScratchpad = new SimpleScratchpad<>(); - final IssHandler handler = new IssHandler( - stateConfig, - new NodeId(selfId), - mock(StatusActionSubmitter.class), - haltRequestedConsumer, - fatalErrorConsumer, - (r, type, otherId) -> {}, - simpleScratchpad); + final IssHandler handler = + new IssHandler(stateConfig, haltRequestedConsumer, fatalErrorConsumer, simpleScratchpad); - handler.selfIssObserver(1234L, randomHash(), randomHash()); + handler.issObserved(new IssNotification(1234L, IssType.SELF_ISS)); assertEquals(0, freezeCount.get(), "unexpected freeze count"); assertEquals(0, shutdownCount.get(), "unexpected shutdown count"); @@ -231,7 +165,6 @@ void selfIssAlwaysFreeze() { .withValue("state.automatedSelfIssRecovery", false) .getOrCreateConfig(); final StateConfig stateConfig = configuration.getConfigData(StateConfig.class); - final long selfId = 0; final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); @@ -241,22 +174,16 @@ void selfIssAlwaysFreeze() { final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); final Scratchpad simpleScratchpad = new SimpleScratchpad<>(); - final IssHandler handler = new IssHandler( - stateConfig, - new NodeId(selfId), - mock(StatusActionSubmitter.class), - haltRequestedConsumer, - fatalErrorConsumer, - (r, type, otherId) -> {}, - simpleScratchpad); + final IssHandler handler = + new IssHandler(stateConfig, haltRequestedConsumer, fatalErrorConsumer, simpleScratchpad); - handler.selfIssObserver(1234L, randomHash(), randomHash()); + handler.issObserved(new IssNotification(1234L, IssType.SELF_ISS)); assertEquals(1, freezeCount.get(), "unexpected freeze count"); assertEquals(0, shutdownCount.get(), "unexpected shutdown count"); // Once frozen, this should become a no-op - handler.selfIssObserver(1234L, randomHash(), randomHash()); + handler.issObserved(new IssNotification(1234L, IssType.SELF_ISS)); assertEquals(1, freezeCount.get(), "unexpected freeze count"); assertEquals(0, shutdownCount.get(), "unexpected shutdown count"); @@ -274,7 +201,6 @@ void catastrophicIssNoAction() { .withValue("state.haltOnCatastrophicIss", false) .getOrCreateConfig(); final StateConfig stateConfig = configuration.getConfigData(StateConfig.class); - final long selfId = 0; final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); @@ -284,16 +210,10 @@ void catastrophicIssNoAction() { final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); final Scratchpad simpleScratchpad = new SimpleScratchpad<>(); - final IssHandler handler = new IssHandler( - stateConfig, - new NodeId(selfId), - mock(StatusActionSubmitter.class), - haltRequestedConsumer, - fatalErrorConsumer, - (r, type, otherId) -> {}, - simpleScratchpad); + final IssHandler handler = + new IssHandler(stateConfig, haltRequestedConsumer, fatalErrorConsumer, simpleScratchpad); - handler.catastrophicIssObserver(1234L, mock(Hash.class)); + handler.issObserved(new IssNotification(1234L, IssType.CATASTROPHIC_ISS)); assertEquals(0, freezeCount.get(), "unexpected freeze count"); assertEquals(0, shutdownCount.get(), "unexpected shutdown count"); @@ -311,7 +231,6 @@ void catastrophicIssAlwaysFreeze() { .withValue("state.haltOnCatastrophicIss", false) .getOrCreateConfig(); final StateConfig stateConfig = configuration.getConfigData(StateConfig.class); - final long selfId = 0; final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); @@ -321,22 +240,16 @@ void catastrophicIssAlwaysFreeze() { final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); final Scratchpad simpleScratchpad = new SimpleScratchpad<>(); - final IssHandler handler = new IssHandler( - stateConfig, - new NodeId(selfId), - mock(StatusActionSubmitter.class), - haltRequestedConsumer, - fatalErrorConsumer, - (r, type, otherId) -> {}, - simpleScratchpad); + final IssHandler handler = + new IssHandler(stateConfig, haltRequestedConsumer, fatalErrorConsumer, simpleScratchpad); - handler.catastrophicIssObserver(1234L, mock(Hash.class)); + handler.issObserved(new IssNotification(1234L, IssType.CATASTROPHIC_ISS)); assertEquals(1, freezeCount.get(), "unexpected freeze count"); assertEquals(0, shutdownCount.get(), "unexpected shutdown count"); // Once frozen, this should become a no-op - handler.catastrophicIssObserver(1234L, mock(Hash.class)); + handler.issObserved(new IssNotification(1234L, IssType.CATASTROPHIC_ISS)); assertEquals(1, freezeCount.get(), "unexpected freeze count"); assertEquals(0, shutdownCount.get(), "unexpected shutdown count"); @@ -354,7 +267,6 @@ void catastrophicIssFreezeOnCatastrophic() { .withValue("state.haltOnCatastrophicIss", true) .getOrCreateConfig(); final StateConfig stateConfig = configuration.getConfigData(StateConfig.class); - final long selfId = 0; final AtomicInteger freezeCount = new AtomicInteger(); final AtomicInteger shutdownCount = new AtomicInteger(); @@ -364,22 +276,16 @@ void catastrophicIssFreezeOnCatastrophic() { final FatalErrorConsumer fatalErrorConsumer = (msg, t, code) -> shutdownCount.getAndIncrement(); final Scratchpad simpleScratchpad = new SimpleScratchpad<>(); - final IssHandler handler = new IssHandler( - stateConfig, - new NodeId(selfId), - mock(StatusActionSubmitter.class), - haltRequestedConsumer, - fatalErrorConsumer, - (r, type, otherId) -> {}, - simpleScratchpad); + final IssHandler handler = + new IssHandler(stateConfig, haltRequestedConsumer, fatalErrorConsumer, simpleScratchpad); - handler.catastrophicIssObserver(1234L, mock(Hash.class)); + handler.issObserved(new IssNotification(1234L, IssType.CATASTROPHIC_ISS)); assertEquals(1, freezeCount.get(), "unexpected freeze count"); assertEquals(0, shutdownCount.get(), "unexpected shutdown count"); // Once frozen, this should become a no-op - handler.catastrophicIssObserver(1234L, mock(Hash.class)); + handler.issObserved(new IssNotification(1234L, IssType.CATASTROPHIC_ISS)); assertEquals(1, freezeCount.get(), "unexpected freeze count"); assertEquals(0, shutdownCount.get(), "unexpected shutdown count"); @@ -388,55 +294,4 @@ void catastrophicIssFreezeOnCatastrophic() { assertNotNull(issRound); assertEquals(issRound.getValue(), 1234L); } - - @Test - @DisplayName("Notifications Test") - void issConsumerTest() { - final AtomicInteger selfIssCount = new AtomicInteger(); - final AtomicInteger otherIssCount = new AtomicInteger(); - final AtomicInteger catastrophicIssCount = new AtomicInteger(); - final IssConsumer issConsumer = (round, type, otherId) -> { - switch (type) { - case OTHER_ISS -> otherIssCount.getAndIncrement(); - case SELF_ISS -> selfIssCount.getAndIncrement(); - case CATASTROPHIC_ISS -> catastrophicIssCount.getAndIncrement(); - } - }; - - final Configuration configuration = new TestConfigBuilder().getOrCreateConfig(); - final StateConfig stateConfig = configuration.getConfigData(StateConfig.class); - final IssHandler issHandler = new IssHandler( - stateConfig, - new NodeId(0L), - mock(StatusActionSubmitter.class), - (reason) -> {}, - (msg, t, code) -> {}, - issConsumer, - new SimpleScratchpad<>()); - - assertEquals(0, selfIssCount.get(), "incorrect self ISS count"); - assertEquals(0, otherIssCount.get(), "incorrect other ISS count"); - assertEquals(0, catastrophicIssCount.get(), "incorrect catastrophic ISS count"); - - issHandler.selfIssObserver(1234L, randomHash(), randomHash()); - assertEquals(1, selfIssCount.get(), "incorrect self ISS count"); - assertEquals(0, otherIssCount.get(), "incorrect other ISS count"); - assertEquals(0, catastrophicIssCount.get(), "incorrect catastrophic ISS count"); - - // This method should not trigger notification when called with the "self" node ID - issHandler.stateHashValidityObserver(4321L, new NodeId(0L), randomHash(), randomHash()); - assertEquals(1, selfIssCount.get(), "incorrect self ISS count"); - assertEquals(0, otherIssCount.get(), "incorrect other ISS count"); - assertEquals(0, catastrophicIssCount.get(), "incorrect catastrophic ISS count"); - - issHandler.stateHashValidityObserver(4321L, new NodeId(7L), randomHash(), randomHash()); - assertEquals(1, selfIssCount.get(), "incorrect self ISS count"); - assertEquals(1, otherIssCount.get(), "incorrect other ISS count"); - assertEquals(0, catastrophicIssCount.get(), "incorrect catastrophic ISS count"); - - issHandler.catastrophicIssObserver(1111L, randomHash()); - assertEquals(1, selfIssCount.get(), "incorrect self ISS count"); - assertEquals(1, otherIssCount.get(), "incorrect other ISS count"); - assertEquals(1, catastrophicIssCount.get(), "incorrect catastrophic ISS count"); - } } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssMetricsTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssMetricsTests.java index 5be8681a8ebc..889e66c33166 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssMetricsTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssMetricsTests.java @@ -124,13 +124,13 @@ void updateTest() { } // Report a catastrophic ISS for a round in the past. Should be ignored. - issMetrics.catastrophicIssObserver(round - 1, null); + issMetrics.catastrophicIssObserver(round - 1); assertEquals(expectedIssCount, issMetrics.getIssCount(), "unexpected ISS count"); assertEquals(expectedIssweight, issMetrics.getIssWeight(), "unexpected ISS weight"); // Report a catastrophic ISS. round++; - issMetrics.catastrophicIssObserver(round, null); + issMetrics.catastrophicIssObserver(round); expectedIssCount = addressBook.getSize(); expectedIssweight = addressBook.getTotalWeight(); assertEquals(expectedIssCount, issMetrics.getIssCount(), "unexpected ISS count"); 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 06bf2c93db50..9988e4db8485 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 @@ -27,7 +27,7 @@ import com.swirlds.common.crypto.Hash; import com.swirlds.common.platform.NodeId; import com.swirlds.common.test.fixtures.RandomAddressBookGenerator; -import com.swirlds.platform.dispatch.triggers.flow.StateHashValidityTrigger; +import com.swirlds.platform.metrics.IssMetrics; import com.swirlds.platform.state.iss.internal.HashValidityStatus; import com.swirlds.platform.state.iss.internal.RoundHashValidator; import com.swirlds.platform.system.address.AddressBook; @@ -42,6 +42,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; @DisplayName("RoundHashValidator Tests") class RoundHashValidatorTests { @@ -53,8 +54,6 @@ static Stream args() { Arguments.of(HashValidityStatus.CATASTROPHIC_ISS)); } - private static final StateHashValidityTrigger NO_OP_DISAGREEMENT_DISPATCHER = (a, b, c, d) -> {}; - record NodeHashInfo(NodeId nodeId, Hash nodeStateHash, long round) {} record HashGenerationData(List nodeList, Hash consensusHash) {} @@ -259,7 +258,7 @@ void selfSignatureLastTest(final HashValidityStatus expectedStatus) { final long round = random.nextInt(1000); final RoundHashValidator validator = - new RoundHashValidator(NO_OP_DISAGREEMENT_DISPATCHER, round, addressBook.getTotalWeight()); + new RoundHashValidator(round, addressBook.getTotalWeight(), Mockito.mock(IssMetrics.class)); boolean decided = false; @@ -305,7 +304,7 @@ void selfSignatureFirstTest(final HashValidityStatus expectedStatus) { final long round = random.nextInt(1000); final RoundHashValidator validator = - new RoundHashValidator(NO_OP_DISAGREEMENT_DISPATCHER, round, addressBook.getTotalWeight()); + new RoundHashValidator(round, addressBook.getTotalWeight(), Mockito.mock(IssMetrics.class)); boolean decided = false; @@ -349,7 +348,7 @@ void selfSignatureInMiddleTest(final HashValidityStatus expectedStatus) { final long round = random.nextInt(1000); final RoundHashValidator validator = - new RoundHashValidator(NO_OP_DISAGREEMENT_DISPATCHER, round, addressBook.getTotalWeight()); + new RoundHashValidator(round, addressBook.getTotalWeight(), Mockito.mock(IssMetrics.class)); boolean decided = false; @@ -399,7 +398,7 @@ void timeoutSelfHashTest() { final long round = random.nextInt(1000); final RoundHashValidator validator = - new RoundHashValidator(NO_OP_DISAGREEMENT_DISPATCHER, round, addressBook.getTotalWeight()); + new RoundHashValidator(round, addressBook.getTotalWeight(), Mockito.mock(IssMetrics.class)); for (final NodeHashInfo nodeHashInfo : hashGenerationData.nodeList) { final NodeId nodeId = nodeHashInfo.nodeId; @@ -433,7 +432,7 @@ void timeoutSelfHashAndSignaturesTest() { final long round = random.nextInt(1000); final RoundHashValidator validator = - new RoundHashValidator(NO_OP_DISAGREEMENT_DISPATCHER, round, addressBook.getTotalWeight()); + new RoundHashValidator(round, addressBook.getTotalWeight(), Mockito.mock(IssMetrics.class)); long addedWeight = 0; @@ -475,7 +474,7 @@ void timeoutSignaturesTest() { final long round = random.nextInt(1000); final RoundHashValidator validator = - new RoundHashValidator(NO_OP_DISAGREEMENT_DISPATCHER, round, addressBook.getTotalWeight()); + new RoundHashValidator(round, addressBook.getTotalWeight(), Mockito.mock(IssMetrics.class)); assertFalse(validator.reportSelfHash(thisNode.nodeStateHash), "should not allow a decision"); @@ -520,7 +519,7 @@ void timeoutWithSuperMajorityTest() { final long round = random.nextInt(1000); final RoundHashValidator validator = - new RoundHashValidator(NO_OP_DISAGREEMENT_DISPATCHER, round, addressBook.getTotalWeight()); + new RoundHashValidator(round, addressBook.getTotalWeight(), Mockito.mock(IssMetrics.class)); assertFalse(validator.reportSelfHash(thisNode.nodeStateHash), "should not allow a decision"); diff --git a/settings.gradle.kts b/settings.gradle.kts index 7148f228aa67..c2d091a2f438 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -134,7 +134,7 @@ fun includeAllProjects(containingFolder: String) { } // The HAPI API version to use for Protobuf sources. -val hapiProtoVersion = "0.44.0" +val hapiProtoVersion = "0.47.0" dependencyResolutionManagement { // Protobuf tool versions @@ -143,6 +143,6 @@ dependencyResolutionManagement { version("grpc-proto", "1.45.1") version("hapi-proto", hapiProtoVersion) - plugin("pbj", "com.hedera.pbj.pbj-compiler").version("0.7.14") + plugin("pbj", "com.hedera.pbj.pbj-compiler").version("0.7.19") } }