From f4a74ca50de9d0467086aa2f998a33b58af9c412 Mon Sep 17 00:00:00 2001 From: Edwin Greene Date: Wed, 22 Jan 2025 10:46:19 -0500 Subject: [PATCH] HIP-1056 Add block file transformer (#10147) - Adds the block file transformer for the cryptotransfer transaction type Signed-off-by: Edwin Greene Signed-off-by: Xin Li Co-authored-by: Xin Li --- docs/design/block-streams.md | 11 +- .../common/domain/transaction/BlockFile.java | 3 +- .../mirror/common/util/DomainUtils.java | 11 + .../mirror/common/domain/DomainBuilder.java | 10 +- .../block/BlockFileTransformer.java | 69 ++++- .../AbstractBlockItemTransformer.java | 62 ++++ .../transformer/BlockItemTransformer.java | 29 ++ .../BlockItemTransformerFactory.java | 65 ++++ .../CryptoTransferTransformer.java | 43 +++ .../block/transformer/UnknownTransformer.java | 29 ++ .../reader/block/BlockRootHashDigest.java | 15 +- .../record/AbstractPreV5RecordFileReader.java | 25 +- .../reader/record/ProtoRecordFileReader.java | 25 +- .../reader/record/RecordFileReaderImplV5.java | 15 +- .../block/BlockFileTransformerTest.java | 291 ++++++++++++++++++ .../AddBlockColumnsMigrationTest.java | 2 +- .../parser/domain/BlockItemBuilder.java | 96 +++--- .../balance/CsvBalanceFileReaderTest.java | 4 +- 18 files changed, 688 insertions(+), 117 deletions(-) create mode 100644 hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/AbstractBlockItemTransformer.java create mode 100644 hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/BlockItemTransformer.java create mode 100644 hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/BlockItemTransformerFactory.java create mode 100644 hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/CryptoTransferTransformer.java create mode 100644 hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/UnknownTransformer.java create mode 100644 hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/downloader/block/BlockFileTransformerTest.java diff --git a/docs/design/block-streams.md b/docs/design/block-streams.md index 40d9e3fbc88..4eb20e58c1f 100644 --- a/docs/design/block-streams.md +++ b/docs/design/block-streams.md @@ -103,18 +103,11 @@ package com.hedera.mirror.importer.downloader.block; public class BlockFileTransformer implements StreamFileTransformer { /** - * Transforms the block file into a record file and calculates the block hash - * The transformation uses a mapping of block fields to record file fields - * Block items are only iterated once in the transform method - * State changes are accumulated and used for calculating the block hash - * - * If the Block File contains a Wrapped Record File, then convert the Wrapped Record File to a Record File + * Transforms the block file into a record file. The transformation uses a mapping of block fields to record file + * fields. Block items are only iterated once in the transform method. */ @Override public RecordFile transform(BlockFile block); - - // The transaction hash will not be included in the block stream output so we will need to calculate it - private byte[] calculateTransactionHash(EventTransaction transaction); } ``` diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/BlockFile.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/BlockFile.java index 0e2148c8929..77b49fd5d27 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/BlockFile.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/BlockFile.java @@ -30,6 +30,7 @@ import lombok.NoArgsConstructor; import lombok.Singular; import lombok.ToString; +import org.apache.commons.lang3.StringUtils; @Builder(toBuilder = true) @Data @@ -92,7 +93,7 @@ public StreamFile copy() { @Override public String getFileHash() { - return null; + return StringUtils.EMPTY; } @Override diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/util/DomainUtils.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/util/DomainUtils.java index 4f7bfb8c15d..5a96d93c462 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/util/DomainUtils.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/util/DomainUtils.java @@ -21,6 +21,7 @@ import com.google.protobuf.Internal; import com.google.protobuf.UnsafeByteOperations; import com.hedera.mirror.common.converter.ObjectToStringSerializer; +import com.hedera.mirror.common.domain.DigestAlgorithm; import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.exception.InvalidEntityException; import com.hedera.mirror.common.exception.ProtobufException; @@ -32,6 +33,8 @@ import jakarta.annotation.Nullable; import java.io.IOException; import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.List; import lombok.CustomLog; @@ -166,6 +169,14 @@ public static long convertToNanosMax(Instant instant) { return convertToNanosMax(instant.getEpochSecond(), instant.getNano()); } + public static MessageDigest createSha384Digest() { + try { + return MessageDigest.getInstance(DigestAlgorithm.SHA_384.getName()); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-384 algorithm not found", e); + } + } + /** * Pad a byte array with leading zeros to a given length. * diff --git a/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/DomainBuilder.java b/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/DomainBuilder.java index 3ae5b9d8688..aa793a766bb 100644 --- a/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/DomainBuilder.java +++ b/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/DomainBuilder.java @@ -1032,7 +1032,7 @@ public DomainWrapper tokenTra public DomainWrapper topicMessage() { var transactionId = TransactionID.newBuilder() .setAccountID(AccountID.newBuilder().setAccountNum(id())) - .setTransactionValidStart(Timestamp.newBuilder().setSeconds(timestamp())) + .setTransactionValidStart(protoTimestamp()) .build() .toByteArray(); var builder = TopicMessage.builder() @@ -1203,6 +1203,14 @@ public void resetTimestamp(long value) { timestampOffset = value - timestampNoOffset(); } + public Timestamp protoTimestamp() { + long timestamp = timestamp(); + return Timestamp.newBuilder() + .setSeconds(timestamp / DomainUtils.NANOS_PER_SECOND) + .setNanos((int) (timestamp % DomainUtils.NANOS_PER_SECOND)) + .build(); + } + public long timestamp() { return timestampNoOffset() + timestampOffset; } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/BlockFileTransformer.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/BlockFileTransformer.java index 48ba2908f91..9c56c2e29ae 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/BlockFileTransformer.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/BlockFileTransformer.java @@ -17,15 +17,82 @@ package com.hedera.mirror.importer.downloader.block; import com.hedera.mirror.common.domain.transaction.BlockFile; +import com.hedera.mirror.common.domain.transaction.BlockItem; import com.hedera.mirror.common.domain.transaction.RecordFile; +import com.hedera.mirror.common.domain.transaction.RecordItem; import com.hedera.mirror.importer.downloader.StreamFileTransformer; +import com.hedera.mirror.importer.downloader.block.transformer.BlockItemTransformerFactory; import jakarta.inject.Named; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.util.Version; @Named +@RequiredArgsConstructor public class BlockFileTransformer implements StreamFileTransformer { + private final BlockItemTransformerFactory blockItemTransformerFactory; + @Override public RecordFile transform(BlockFile blockFile) { - return RecordFile.builder().build(); + var blockHeader = blockFile.getBlockHeader(); + var hapiProtoVersion = blockHeader.getHapiProtoVersion(); + int major = hapiProtoVersion.getMajor(); + int minor = hapiProtoVersion.getMinor(); + int patch = hapiProtoVersion.getPatch(); + var hapiVersion = new Version(major, minor, patch); + var softwareVersion = blockHeader.getSoftwareVersion(); + return RecordFile.builder() + .bytes(blockFile.getBytes()) + .consensusEnd(blockFile.getConsensusEnd()) + .consensusStart(blockFile.getConsensusStart()) + .count(blockFile.getCount()) + .digestAlgorithm(blockFile.getDigestAlgorithm()) + .fileHash(StringUtils.EMPTY) + .hapiVersionMajor(major) + .hapiVersionMinor(minor) + .hapiVersionPatch(patch) + .hash(blockFile.getHash()) + .index(blockHeader.getNumber()) + .items(getRecordItems(blockFile.getItems(), hapiVersion)) + .loadEnd(blockFile.getLoadEnd()) + .loadStart(blockFile.getLoadStart()) + .name(blockFile.getName()) + .nodeId(blockFile.getNodeId()) + .previousHash(blockFile.getPreviousHash()) + .roundEnd(blockFile.getRoundEnd()) + .roundStart(blockFile.getRoundStart()) + .size(blockFile.getSize()) + .softwareVersionMajor(softwareVersion.getMajor()) + .softwareVersionMinor(softwareVersion.getMinor()) + .softwareVersionPatch(softwareVersion.getPatch()) + .version(blockFile.getVersion()) + .build(); + } + + private List getRecordItems(Collection blockItems, Version hapiVersion) { + if (blockItems.isEmpty()) { + return Collections.emptyList(); + } + + RecordItem previousItem = null; + var recordItems = new ArrayList(blockItems.size()); + for (var blockItem : blockItems) { + var recordItem = RecordItem.builder() + .hapiVersion(hapiVersion) + .previous(previousItem) + .transaction(blockItem.transaction()) + .transactionIndex(recordItems.size()) + .transactionRecord(blockItemTransformerFactory.getTransactionRecord(blockItem)) + .build(); + recordItems.add(recordItem); + previousItem = recordItem; + } + + return recordItems; } } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/AbstractBlockItemTransformer.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/AbstractBlockItemTransformer.java new file mode 100644 index 00000000000..7f8df331974 --- /dev/null +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/AbstractBlockItemTransformer.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2025 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.importer.downloader.block.transformer; + +import static com.hedera.mirror.common.util.DomainUtils.createSha384Digest; + +import com.google.protobuf.ByteString; +import com.hedera.mirror.common.domain.transaction.BlockItem; +import com.hedera.mirror.common.util.DomainUtils; +import com.hederahashgraph.api.proto.java.TransactionBody; +import com.hederahashgraph.api.proto.java.TransactionReceipt; +import com.hederahashgraph.api.proto.java.TransactionRecord; +import java.security.MessageDigest; + +abstract class AbstractBlockItemTransformer implements BlockItemTransformer { + + private static final MessageDigest DIGEST = createSha384Digest(); + + public TransactionRecord getTransactionRecord(BlockItem blockItem, TransactionBody transactionBody) { + var transactionResult = blockItem.transactionResult(); + var receiptBuilder = TransactionReceipt.newBuilder().setStatus(transactionResult.getStatus()); + var transactionRecordBuilder = TransactionRecord.newBuilder() + .addAllAutomaticTokenAssociations(transactionResult.getAutomaticTokenAssociationsList()) + .addAllPaidStakingRewards(transactionResult.getPaidStakingRewardsList()) + .addAllTokenTransferLists(transactionResult.getTokenTransferListsList()) + .setConsensusTimestamp(transactionResult.getConsensusTimestamp()) + .setParentConsensusTimestamp(transactionResult.getParentConsensusTimestamp()) + .setMemo(transactionBody.getMemo()) + .setReceipt(receiptBuilder) + .setScheduleRef(transactionResult.getScheduleRef()) + .setTransactionFee(transactionResult.getTransactionFeeCharged()) + .setTransactionHash( + calculateTransactionHash(blockItem.transaction().getSignedTransactionBytes())) + .setTransactionID(transactionBody.getTransactionID()) + .setTransferList(transactionResult.getTransferList()); + + updateTransactionRecord(blockItem, transactionRecordBuilder); + return transactionRecordBuilder.build(); + } + + protected void updateTransactionRecord(BlockItem blockItem, TransactionRecord.Builder transactionRecordBuilder) { + // do nothing + } + + private ByteString calculateTransactionHash(ByteString signedTransactionBytes) { + return DomainUtils.fromBytes(DIGEST.digest(DomainUtils.toBytes(signedTransactionBytes))); + } +} diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/BlockItemTransformer.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/BlockItemTransformer.java new file mode 100644 index 00000000000..0349b82297f --- /dev/null +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/BlockItemTransformer.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.importer.downloader.block.transformer; + +import com.hedera.mirror.common.domain.transaction.BlockItem; +import com.hedera.mirror.common.domain.transaction.TransactionType; +import com.hederahashgraph.api.proto.java.TransactionBody; +import com.hederahashgraph.api.proto.java.TransactionRecord; + +interface BlockItemTransformer { + + TransactionRecord getTransactionRecord(BlockItem blockItem, TransactionBody transactionBody); + + TransactionType getType(); +} diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/BlockItemTransformerFactory.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/BlockItemTransformerFactory.java new file mode 100644 index 00000000000..4cb9cf6a765 --- /dev/null +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/BlockItemTransformerFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2025 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.importer.downloader.block.transformer; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.hedera.mirror.common.domain.transaction.BlockItem; +import com.hedera.mirror.common.domain.transaction.TransactionType; +import com.hedera.mirror.common.exception.ProtobufException; +import com.hederahashgraph.api.proto.java.SignedTransaction; +import com.hederahashgraph.api.proto.java.TransactionBody; +import com.hederahashgraph.api.proto.java.TransactionRecord; +import jakarta.inject.Named; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Named +public class BlockItemTransformerFactory { + + private final BlockItemTransformer defaultTransformer; + private final Map transformers; + + BlockItemTransformerFactory(List transformers) { + this.transformers = transformers.stream() + .collect(Collectors.toUnmodifiableMap(BlockItemTransformer::getType, Function.identity())); + this.defaultTransformer = this.transformers.get(TransactionType.UNKNOWN); + } + + public TransactionRecord getTransactionRecord(BlockItem blockItem) { + var transactionBody = parse(blockItem.transaction().getSignedTransactionBytes()); + var blockItemTransformer = get(transactionBody); + // pass transactionBody for performance + return blockItemTransformer.getTransactionRecord(blockItem, transactionBody); + } + + private BlockItemTransformer get(TransactionBody transactionBody) { + var transactionType = TransactionType.of(transactionBody.getDataCase().getNumber()); + return transformers.getOrDefault(transactionType, defaultTransformer); + } + + private TransactionBody parse(ByteString signedTransactionBytes) { + try { + var signedTransaction = SignedTransaction.parseFrom(signedTransactionBytes); + return TransactionBody.parseFrom(signedTransaction.getBodyBytes()); + } catch (InvalidProtocolBufferException e) { + throw new ProtobufException("Error parsing transaction body from signed transaction bytes", e); + } + } +} diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/CryptoTransferTransformer.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/CryptoTransferTransformer.java new file mode 100644 index 00000000000..808161802fb --- /dev/null +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/CryptoTransferTransformer.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.importer.downloader.block.transformer; + +import com.hedera.mirror.common.domain.transaction.BlockItem; +import com.hedera.mirror.common.domain.transaction.TransactionType; +import com.hederahashgraph.api.proto.java.TransactionRecord; +import jakarta.inject.Named; + +@Named +class CryptoTransferTransformer extends AbstractBlockItemTransformer { + + @Override + protected void updateTransactionRecord(BlockItem blockItem, TransactionRecord.Builder transactionRecordBuilder) { + for (var transactionOutput : blockItem.transactionOutput()) { + if (transactionOutput.hasCryptoTransfer()) { + var cryptoTransferOutput = transactionOutput.getCryptoTransfer(); + var assessedCustomFees = cryptoTransferOutput.getAssessedCustomFeesList(); + transactionRecordBuilder.addAllAssessedCustomFees(assessedCustomFees); + break; + } + } + } + + @Override + public TransactionType getType() { + return TransactionType.CRYPTOTRANSFER; + } +} diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/UnknownTransformer.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/UnknownTransformer.java new file mode 100644 index 00000000000..d7862475fc9 --- /dev/null +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/downloader/block/transformer/UnknownTransformer.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.importer.downloader.block.transformer; + +import com.hedera.mirror.common.domain.transaction.TransactionType; +import jakarta.inject.Named; + +@Named +class UnknownTransformer extends AbstractBlockItemTransformer { + + @Override + public TransactionType getType() { + return TransactionType.UNKNOWN; + } +} diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/block/BlockRootHashDigest.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/block/BlockRootHashDigest.java index 7b196cb40ba..975bc7e9dc9 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/block/BlockRootHashDigest.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/block/BlockRootHashDigest.java @@ -17,12 +17,11 @@ package com.hedera.mirror.importer.reader.block; import static com.hedera.mirror.common.domain.DigestAlgorithm.SHA_384; +import static com.hedera.mirror.common.util.DomainUtils.createSha384Digest; import com.hedera.hapi.block.stream.protoc.BlockItem; import com.hedera.mirror.common.util.DomainUtils; -import com.hedera.mirror.importer.exception.StreamFileReaderException; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -36,9 +35,9 @@ @Data class BlockRootHashDigest { - private static final byte[] EMPTY_HASH = createMessageDigest().digest(new byte[0]); + private static final byte[] EMPTY_HASH = createSha384Digest().digest(new byte[0]); - private final MessageDigest digest = createMessageDigest(); + private final MessageDigest digest = createSha384Digest(); private boolean finalized; private final List inputHashes = new ArrayList<>(); private final List outputHashes = new ArrayList<>(); @@ -113,14 +112,6 @@ private byte[] getRootHash(List leaves) { return leaves.getFirst(); } - private static MessageDigest createMessageDigest() { - try { - return MessageDigest.getInstance(SHA_384.getName()); - } catch (NoSuchAlgorithmException ex) { - throw new StreamFileReaderException(ex); - } - } - private static void validateHash(byte[] hash, String name) { if (Objects.requireNonNull(hash, "Null " + name).length != SHA_384.getSize()) { throw new IllegalArgumentException(String.format("%s is not %d bytes", name, SHA_384.getSize())); diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/AbstractPreV5RecordFileReader.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/AbstractPreV5RecordFileReader.java index 072d8df423a..d6bd690890c 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/AbstractPreV5RecordFileReader.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/AbstractPreV5RecordFileReader.java @@ -16,6 +16,8 @@ package com.hedera.mirror.importer.reader.record; +import static com.hedera.mirror.common.util.DomainUtils.createSha384Digest; + import com.hedera.mirror.common.domain.DigestAlgorithm; import com.hedera.mirror.common.domain.transaction.RecordFile; import com.hedera.mirror.common.domain.transaction.RecordItem; @@ -29,7 +31,6 @@ import java.io.InputStream; import java.security.DigestInputStream; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import lombok.Getter; @@ -157,19 +158,15 @@ protected static class RecordFileDigest implements AutoCloseable { private final MessageDigest messageDigestBody; public RecordFileDigest(InputStream is, boolean simple) { - try { - messageDigestFile = MessageDigest.getInstance(DIGEST_ALGORITHM.getName()); - digestInputStream = new DigestInputStream(is, messageDigestFile); - - if (simple) { - messageDigestBody = null; - } else { - // calculate the hash of the body separately, and the file hash is calculated as - // h(header | h(body)) - messageDigestBody = MessageDigest.getInstance(DIGEST_ALGORITHM.getName()); - } - } catch (NoSuchAlgorithmException e) { - throw new StreamFileReaderException("Unable to instantiate RecordFileDigest", e); + messageDigestFile = createSha384Digest(); + digestInputStream = new DigestInputStream(is, messageDigestFile); + + if (simple) { + messageDigestBody = null; + } else { + // calculate the hash of the body separately, and the file hash is calculated as + // h(header | h(body)) + messageDigestBody = createSha384Digest(); } } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/ProtoRecordFileReader.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/ProtoRecordFileReader.java index 524abd48275..e35e46d91f9 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/ProtoRecordFileReader.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/ProtoRecordFileReader.java @@ -16,6 +16,7 @@ package com.hedera.mirror.importer.reader.record; +import static com.hedera.mirror.common.util.DomainUtils.createSha384Digest; import static java.lang.String.format; import com.hedera.mirror.common.domain.DigestAlgorithm; @@ -26,7 +27,6 @@ import com.hedera.mirror.importer.domain.StreamFileData; import com.hedera.mirror.importer.domain.StreamFilename; import com.hedera.mirror.importer.exception.InvalidStreamFileException; -import com.hedera.mirror.importer.exception.StreamFileReaderException; import com.hedera.services.stream.proto.HashAlgorithm; import com.hedera.services.stream.proto.RecordStreamFile; import jakarta.inject.Named; @@ -35,8 +35,6 @@ import java.io.IOException; import java.io.InputStream; import java.security.DigestOutputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -88,7 +86,7 @@ public RecordFile read(StreamFileData streamFileData) { .consensusEnd(consensusEnd) .count((long) count) .digestAlgorithm(digestAlgorithm) - .fileHash(getFileHash(digestAlgorithm, streamFileData.getDecompressedBytes())) + .fileHash(getFileHash(streamFileData.getDecompressedBytes())) .hapiVersionMajor(majorVersion) .hapiVersionMinor(minorVersion) .hapiVersionPatch(patchVersion) @@ -96,7 +94,7 @@ public RecordFile read(StreamFileData streamFileData) { .index(recordStreamFile.getBlockNumber()) .items(items) .loadStart(loadStart) - .metadataHash(getMetadataHash(digestAlgorithm, recordStreamFile)) + .metadataHash(getMetadataHash(recordStreamFile)) .name(filename) .previousHash(DomainUtils.bytesToHex(DomainUtils.getHashBytes(startObjectRunningHash))) .sidecarCount(sidecars.size()) @@ -112,14 +110,6 @@ public RecordFile read(StreamFileData streamFileData) { } } - private MessageDigest createMessageDigest(DigestAlgorithm digestAlgorithm) { - try { - return MessageDigest.getInstance(digestAlgorithm.getName()); - } catch (NoSuchAlgorithmException e) { - throw new StreamFileReaderException(e); - } - } - private DigestAlgorithm getDigestAlgorithm(String filename, HashAlgorithm start, HashAlgorithm end) { return Stream.of(start, end) .map(hashAlgorithm -> { @@ -140,14 +130,13 @@ private DigestAlgorithm getDigestAlgorithm(String filename, HashAlgorithm start, }); } - private String getFileHash(DigestAlgorithm algorithm, byte[] fileData) { - var messageDigest = createMessageDigest(algorithm); + private String getFileHash(byte[] fileData) { + var messageDigest = createSha384Digest(); return DomainUtils.bytesToHex(messageDigest.digest(fileData)); } - private String getMetadataHash(DigestAlgorithm algorithm, RecordStreamFile recordStreamFile) throws IOException { - try (var digestOutputStream = - new DigestOutputStream(NullOutputStream.INSTANCE, createMessageDigest(algorithm)); + private String getMetadataHash(RecordStreamFile recordStreamFile) throws IOException { + try (var digestOutputStream = new DigestOutputStream(NullOutputStream.INSTANCE, createSha384Digest()); var dataOutputStream = new DataOutputStream(digestOutputStream)) { var hapiProtoVersion = recordStreamFile.getHapiProtoVersion(); dataOutputStream.writeInt(VERSION); diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/RecordFileReaderImplV5.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/RecordFileReaderImplV5.java index fccbd88b387..613da6f8c53 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/RecordFileReaderImplV5.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/RecordFileReaderImplV5.java @@ -16,6 +16,8 @@ package com.hedera.mirror.importer.reader.record; +import static com.hedera.mirror.common.util.DomainUtils.createSha384Digest; + import com.google.common.primitives.Longs; import com.hedera.mirror.common.domain.DigestAlgorithm; import com.hedera.mirror.common.domain.transaction.RecordFile; @@ -34,7 +36,6 @@ import java.io.IOException; import java.security.DigestInputStream; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import lombok.EqualsAndHashCode; @@ -49,8 +50,8 @@ public class RecordFileReaderImplV5 implements RecordFileReader { @Override public RecordFile read(StreamFileData streamFileData) { - MessageDigest messageDigestFile = createMessageDigest(DIGEST_ALGORITHM); - MessageDigest messageDigestMetadata = createMessageDigest(DIGEST_ALGORITHM); + MessageDigest messageDigestFile = createSha384Digest(); + MessageDigest messageDigestMetadata = createSha384Digest(); String filename = streamFileData.getFilename(); // the first DigestInputStream is for file hash and the second is for metadata hash. Any BufferedInputStream @@ -151,14 +152,6 @@ private void readBody( recordFile.setPreviousHash(Hex.encodeHexString(startHashObject.getHash())); } - private MessageDigest createMessageDigest(DigestAlgorithm digestAlgorithm) { - try { - return MessageDigest.getInstance(digestAlgorithm.getName()); - } catch (NoSuchAlgorithmException e) { - throw new StreamFileReaderException(e); - } - } - private boolean isHashObject(DataInputStream dis, long hashObjectClassId) throws IOException { dis.mark(Longs.BYTES); long classId = dis.readLong(); diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/downloader/block/BlockFileTransformerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/downloader/block/BlockFileTransformerTest.java new file mode 100644 index 00000000000..394ccf3aa4a --- /dev/null +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/downloader/block/BlockFileTransformerTest.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2025 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.importer.downloader.block; + +import static com.hedera.mirror.common.util.DomainUtils.createSha384Digest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.protobuf.ByteString; +import com.hedera.hapi.block.stream.output.protoc.BlockHeader; +import com.hedera.hapi.block.stream.output.protoc.TransactionResult; +import com.hedera.mirror.common.domain.DigestAlgorithm; +import com.hedera.mirror.common.domain.transaction.BlockFile; +import com.hedera.mirror.common.domain.transaction.BlockItem; +import com.hedera.mirror.common.domain.transaction.RecordFile; +import com.hedera.mirror.common.domain.transaction.RecordItem; +import com.hedera.mirror.common.exception.ProtobufException; +import com.hedera.mirror.common.util.DomainUtils; +import com.hedera.mirror.importer.ImporterIntegrationTest; +import com.hedera.mirror.importer.parser.domain.BlockItemBuilder; +import com.hedera.mirror.importer.parser.domain.RecordItemBuilder; +import com.hedera.mirror.importer.parser.domain.RecordItemBuilder.TransferType; +import com.hederahashgraph.api.proto.java.SemanticVersion; +import com.hederahashgraph.api.proto.java.SignedTransaction; +import com.hederahashgraph.api.proto.java.Transaction; +import com.hederahashgraph.api.proto.java.TransactionRecord; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.data.util.Version; + +@RequiredArgsConstructor +class BlockFileTransformerTest extends ImporterIntegrationTest { + + private static final int HAPI_VERSION_MINOR = 57; + private static final Version HAPI_VERSION = new Version(0, HAPI_VERSION_MINOR); + + private final BlockItemBuilder blockItemBuilder = new BlockItemBuilder(); + private final BlockFileTransformer blockFileTransformer; + private final RecordItemBuilder recordItemBuilder = new RecordItemBuilder(); + + @ParameterizedTest + @EnumSource(value = TransferType.class) + void cryptoTransfer(TransferType transferType) { + // given + var expectedRecordItem = recordItemBuilder + .cryptoTransfer(transferType) + .recordItem(r -> r.hapiVersion(HAPI_VERSION)) + .build(); + var expectedTransactionHash = getExpectedTransactionHash(expectedRecordItem); + + var expectedRecordItem2 = recordItemBuilder + .cryptoTransfer(transferType) + .recordItem(r -> r.hapiVersion(HAPI_VERSION).transactionIndex(1)) + .build(); + var expectedTransactionHash2 = getExpectedTransactionHash(expectedRecordItem2); + + var blockItem1 = blockItemBuilder.cryptoTransfer(expectedRecordItem).build(); + var expectedFees = + blockItem1.transactionOutput().get(1).getCryptoTransfer().getAssessedCustomFeesList(); + var blockItem2 = blockItemBuilder.cryptoTransfer(expectedRecordItem2).build(); + var expectedFees2 = + blockItem2.transactionOutput().get(1).getCryptoTransfer().getAssessedCustomFeesList(); + var blockFile = blockFile(List.of(blockItem1, blockItem2)); + + // when + var recordFile = blockFileTransformer.transform(blockFile); + + // then + assertRecordFile(recordFile, blockFile, items -> { + assertThat(items).hasSize(2); + assertThat(items) + .element(0) + .satisfies(recordItem -> assertRecordItem(recordItem, expectedRecordItem)) + .returns(null, RecordItem::getPrevious) + .extracting(RecordItem::getTransactionRecord) + .returns(expectedTransactionHash, TransactionRecord::getTransactionHash) + .returns(expectedFees, TransactionRecord::getAssessedCustomFeesList); + assertThat(items) + .element(1) + .satisfies(recordItem -> assertRecordItem(recordItem, expectedRecordItem2)) + .returns(items.iterator().next(), RecordItem::getPrevious) + .extracting(RecordItem::getTransactionRecord) + .returns(expectedTransactionHash2, TransactionRecord::getTransactionHash) + .returns(expectedFees2, TransactionRecord::getAssessedCustomFeesList); + }); + } + + @Test + void corruptedTransactionBodyBytes() { + // given + var blockItem = new BlockItem( + Transaction.newBuilder() + .setSignedTransactionBytes(SignedTransaction.newBuilder() + .setBodyBytes(DomainUtils.fromBytes(domainBuilder.bytes(512))) + .build() + .toByteString()) + .build(), + TransactionResult.newBuilder().build(), + Collections.emptyList(), + Collections.emptyList()); + var blockFile = blockFile(List.of(blockItem)); + + // when, then + assertThatThrownBy(() -> blockFileTransformer.transform(blockFile)).isInstanceOf(ProtobufException.class); + } + + @Test + void corruptedSignedTransactionBytes() { + // given + var blockItem = new BlockItem( + Transaction.newBuilder() + .setSignedTransactionBytes(DomainUtils.fromBytes(domainBuilder.bytes(256))) + .build(), + TransactionResult.newBuilder().build(), + Collections.emptyList(), + Collections.emptyList()); + var blockFile = blockFile(List.of(blockItem)); + + // when, then + assertThatThrownBy(() -> blockFileTransformer.transform(blockFile)).isInstanceOf(ProtobufException.class); + } + + @Test + void emptyBlockFile() { + // given + var blockFile = blockFile(Collections.emptyList()); + + // when + var recordFile = blockFileTransformer.transform(blockFile); + + // then + assertThat(recordFile.getItems()).isEmpty(); + assertRecordFile(recordFile, blockFile, items -> {}); + } + + @Test + void unknownTransform() { + // given + var expectedRecordItem = recordItemBuilder + .unknown() + .recordItem(r -> r.hapiVersion(HAPI_VERSION)) + .build(); + var blockItem = blockItemBuilder.unknown(expectedRecordItem).build(); + var blockFile = blockFile(List.of(blockItem)); + + // when + var recordFile = blockFileTransformer.transform(blockFile); + + // then + assertRecordFile(recordFile, blockFile, items -> { + assertThat(items).hasSize(1).first().satisfies(item -> assertRecordItem(item, expectedRecordItem)); + }); + } + + private void assertRecordFile( + RecordFile actual, BlockFile blockFile, Consumer> itemsAssert) { + var hapiProtoVersion = blockFile.getBlockHeader().getHapiProtoVersion(); + var softwareVersion = blockFile.getBlockHeader().getSoftwareVersion(); + assertThat(actual) + .returns(blockFile.getBytes(), RecordFile::getBytes) + .returns(blockFile.getConsensusEnd(), RecordFile::getConsensusEnd) + .returns(blockFile.getConsensusStart(), RecordFile::getConsensusStart) + .returns(blockFile.getCount(), RecordFile::getCount) + .returns(blockFile.getDigestAlgorithm(), RecordFile::getDigestAlgorithm) + .returns(StringUtils.EMPTY, RecordFile::getFileHash) + .returns(0L, RecordFile::getGasUsed) + .returns(hapiProtoVersion.getMajor(), RecordFile::getHapiVersionMajor) + .returns(hapiProtoVersion.getMinor(), RecordFile::getHapiVersionMinor) + .returns(hapiProtoVersion.getPatch(), RecordFile::getHapiVersionPatch) + .returns(blockFile.getHash(), RecordFile::getHash) + .returns(blockFile.getIndex(), RecordFile::getIndex) + .returns(null, RecordFile::getLoadEnd) + .returns(blockFile.getLoadStart(), RecordFile::getLoadStart) + .returns(null, RecordFile::getLogsBloom) + .returns(null, RecordFile::getMetadataHash) + .returns(blockFile.getName(), RecordFile::getName) + .returns(blockFile.getNodeId(), RecordFile::getNodeId) + .returns(blockFile.getPreviousHash(), RecordFile::getPreviousHash) + .returns(blockFile.getRoundEnd(), RecordFile::getRoundEnd) + .returns(blockFile.getRoundStart(), RecordFile::getRoundStart) + .returns(0, RecordFile::getSidecarCount) + .satisfies(r -> assertThat(r.getSidecars()).isEmpty()) + .returns(blockFile.getSize(), RecordFile::getSize) + .returns(softwareVersion.getMajor(), RecordFile::getSoftwareVersionMajor) + .returns(softwareVersion.getMinor(), RecordFile::getSoftwareVersionMinor) + .returns(softwareVersion.getPatch(), RecordFile::getSoftwareVersionPatch) + .returns(blockFile.getVersion(), RecordFile::getVersion) + .extracting(RecordFile::getItems) + .satisfies(itemsAssert); + } + + private void assertRecordItem(RecordItem recordItem, RecordItem expectedRecordItem) { + assertThat(recordItem.getTransactionRecord().getMemo()) + .isEqualTo(expectedRecordItem.getTransactionRecord().getMemo()); + assertThat(recordItem) + .usingRecursiveComparison() + .ignoringFields( + "contractTransactionPredicate", + "entityTransactionPredicate", + "previous", + // Memo omitted here as this compares a ByteString to a String. + // The end result of the parsed memo value in persistence is equivalent whether the record file + // contains memoBytes or a memo String value. + "transactionRecord.memo_", + "transactionRecord.receipt_.memoizedIsInitialized", + "transactionRecord.assessedCustomFees_", + "transactionRecord.scheduleRef_", + "transactionRecord.parentConsensusTimestamp_", + // Record file builder transaction hash is not generated based on transaction bytes, so these + // will not match + "transactionRecord.transactionHash_", + "transactionRecord.transactionID_.accountID_.memoizedHashCode", + "transactionRecord.transactionID_.accountID_.memoizedIsInitialized", + "transactionRecord.transactionID_.accountID_.memoizedSize", + "transactionRecord.transactionID_.memoizedIsInitialized", + "transactionRecord.transactionID_.memoizedSize", + "transactionRecord.transactionID_.transactionValidStart_.memoizedIsInitialized", + "transactionRecord.transactionID_.transactionValidStart_.memoizedSize") + .isEqualTo(expectedRecordItem); + } + + private ByteString getExpectedTransactionHash(RecordItem recordItem) { + var digest = createSha384Digest(); + return ByteString.copyFrom( + digest.digest(DomainUtils.toBytes(recordItem.getTransaction().getSignedTransactionBytes()))); + } + + private BlockFile blockFile(List blockItems) { + long blockNumber = domainBuilder.number(); + byte[] bytes = domainBuilder.bytes(256); + String filename = StringUtils.leftPad(Long.toString(blockNumber), 36, "0") + ".blk.gz"; + var firstConsensusTimestamp = blockItems.isEmpty() + ? domainBuilder.protoTimestamp() + : blockItems.getFirst().transactionResult().getConsensusTimestamp(); + byte[] previousHash = domainBuilder.bytes(48); + long consensusStart = DomainUtils.timestampInNanosMax(firstConsensusTimestamp); + long consensusEnd = blockItems.isEmpty() + ? consensusStart + : DomainUtils.timestampInNanosMax( + blockItems.getLast().transactionResult().getConsensusTimestamp()); + + return BlockFile.builder() + .blockHeader(BlockHeader.newBuilder() + .setFirstTransactionConsensusTime(firstConsensusTimestamp) + .setNumber(blockNumber) + .setPreviousBlockHash(DomainUtils.fromBytes(previousHash)) + .setHapiProtoVersion(SemanticVersion.newBuilder().setMinor(HAPI_VERSION_MINOR)) + .setSoftwareVersion(SemanticVersion.newBuilder() + .setMinor(HAPI_VERSION_MINOR) + .setPatch(1)) + .build()) + .bytes(bytes) + .consensusEnd(consensusEnd) + .consensusStart(consensusStart) + .count((long) blockItems.size()) + .digestAlgorithm(DigestAlgorithm.SHA_384) + .hash(DomainUtils.bytesToHex(domainBuilder.bytes(48))) + .index(blockNumber) + .items(blockItems) + .loadStart(System.currentTimeMillis()) + .name(filename) + .nodeId(domainBuilder.number()) + .previousHash(DomainUtils.bytesToHex(previousHash)) + .roundEnd(blockNumber + 1) + .roundStart(blockNumber + 1) + .size(bytes.length) + .version(7) + .build(); + } +} diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/migration/AddBlockColumnsMigrationTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/migration/AddBlockColumnsMigrationTest.java index 98e469a6778..f47127654ba 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/migration/AddBlockColumnsMigrationTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/migration/AddBlockColumnsMigrationTest.java @@ -44,7 +44,7 @@ @DisableRepeatableSqlMigration @RequiredArgsConstructor @Tag("migration") -public class AddBlockColumnsMigrationTest extends RecordFileMigrationTest { +class AddBlockColumnsMigrationTest extends RecordFileMigrationTest { private static final String REVERT_DDL = """ diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/domain/BlockItemBuilder.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/domain/BlockItemBuilder.java index 453709be044..e67b67833d6 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/domain/BlockItemBuilder.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/domain/BlockItemBuilder.java @@ -16,20 +16,20 @@ package com.hedera.mirror.importer.parser.domain; -import com.google.protobuf.GeneratedMessageV3; import com.hedera.hapi.block.stream.output.protoc.CallContractOutput; import com.hedera.hapi.block.stream.output.protoc.CryptoTransferOutput; import com.hedera.hapi.block.stream.output.protoc.StateChanges; import com.hedera.hapi.block.stream.output.protoc.TransactionOutput; import com.hedera.hapi.block.stream.output.protoc.TransactionResult; import com.hedera.mirror.common.domain.transaction.BlockItem; -import com.hedera.mirror.common.domain.transaction.TransactionType; +import com.hedera.mirror.common.domain.transaction.RecordItem; +import com.hedera.mirror.importer.util.Utility; import com.hederahashgraph.api.proto.java.AssessedCustomFee; -import com.hederahashgraph.api.proto.java.CryptoTransferTransactionBody; -import com.hederahashgraph.api.proto.java.SignedTransaction; +import com.hederahashgraph.api.proto.java.Timestamp; import com.hederahashgraph.api.proto.java.Transaction; -import com.hederahashgraph.api.proto.java.TransactionBody; +import com.hederahashgraph.api.proto.java.TransactionRecord; import jakarta.inject.Named; +import java.time.Instant; import java.util.Collections; import java.util.List; import org.springframework.beans.factory.config.ConfigurableBeanFactory; @@ -44,13 +44,16 @@ public class BlockItemBuilder { private final RecordItemBuilder recordItemBuilder = new RecordItemBuilder(); - public BlockItemBuilder.Builder cryptoTransfer() { + public BlockItemBuilder.Builder cryptoTransfer() { var recordItem = recordItemBuilder.cryptoTransfer().build(); - var transaction = recordItem.getTransactionBody(); - var transactionBody = transaction.toBuilder().getCryptoTransferBuilder(); - var transactionResult = TransactionResult.newBuilder() - .setTransferList(transaction.getCryptoTransfer().getTransfers()) - .build(); + return cryptoTransfer(recordItem); + } + + public BlockItemBuilder.Builder cryptoTransfer(RecordItem recordItem) { + var instant = Instant.ofEpochSecond(0, recordItem.getConsensusTimestamp()); + var timestamp = Utility.instantToTimestamp(instant); + var transactionRecord = recordItem.getTransactionRecord(); + var transactionResult = transactionResult(transactionRecord, timestamp).build(); var contractCallTransactionOutput = TransactionOutput.newBuilder() .setContractCall(CallContractOutput.newBuilder() @@ -63,14 +66,28 @@ public BlockItemBuilder.Builder cryptoTra .build()) .build(); - return new BlockItemBuilder.Builder<>( - TransactionType.CRYPTOTRANSFER, - transactionBody, + return new BlockItemBuilder.Builder( + recordItem.getTransaction(), transactionResult, List.of(contractCallTransactionOutput, cryptoTransferTransactionOutput), Collections.emptyList()); } + public BlockItemBuilder.Builder unknown() { + var recordItem = recordItemBuilder.unknown().build(); + return unknown(recordItem); + } + + public BlockItemBuilder.Builder unknown(RecordItem recordItem) { + var instant = Instant.ofEpochSecond(0, recordItem.getConsensusTimestamp()); + var timestamp = Utility.instantToTimestamp(instant); + var transactionRecord = recordItem.getTransactionRecord(); + var transactionResult = transactionResult(transactionRecord, timestamp).build(); + + return new BlockItemBuilder.Builder( + recordItem.getTransaction(), transactionResult, List.of(), Collections.emptyList()); + } + private AssessedCustomFee.Builder assessedCustomFees() { return AssessedCustomFee.newBuilder() .setAmount(1L) @@ -79,53 +96,38 @@ private AssessedCustomFee.Builder assessedCustomFees() { .setTokenId(recordItemBuilder.tokenId()); } - public class Builder> { - private final TransactionType type; - private final T transactionBody; - private final TransactionBody.Builder transactionBodyWrapper; + private TransactionResult.Builder transactionResult( + TransactionRecord transactionRecord, Timestamp consensusTimestamp) { + return TransactionResult.newBuilder() + .addAllPaidStakingRewards(transactionRecord.getPaidStakingRewardsList()) + .addAllTokenTransferLists(transactionRecord.getTokenTransferListsList()) + .setConsensusTimestamp(consensusTimestamp) + .setParentConsensusTimestamp(transactionRecord.getParentConsensusTimestamp()) + .setScheduleRef(transactionRecord.getScheduleRef()) + .setTransferList(transactionRecord.getTransferList()) + .setTransactionFeeCharged(transactionRecord.getTransactionFee()) + .setStatus(transactionRecord.getReceipt().getStatus()); + } + + public class Builder { + private final Transaction transaction; private final List transactionOutputs; private final TransactionResult transactionResult; private final List stateChanges; - private final BlockItem.BlockItemBuilder blockItemBuilder; private Builder( - TransactionType type, - T transactionBody, + Transaction transaction, TransactionResult transactionResult, List transactionOutputs, List stateChanges) { - this.blockItemBuilder = BlockItem.builder(); this.stateChanges = stateChanges; - this.type = type; - this.transactionBody = transactionBody; - this.transactionBodyWrapper = defaultTransactionBody(); + this.transaction = transaction; this.transactionOutputs = transactionOutputs; this.transactionResult = transactionResult; } - private TransactionBody.Builder defaultTransactionBody() { - return TransactionBody.newBuilder().setMemo(type.name()); - } - - private Transaction.Builder transaction() { - return Transaction.newBuilder() - .setSignedTransactionBytes(SignedTransaction.newBuilder() - .setBodyBytes(transactionBodyWrapper.build().toByteString()) - .build() - .toByteString()); - } - public BlockItem build() { - var field = transactionBodyWrapper.getDescriptorForType().findFieldByNumber(type.getProtoId()); - if (field != null) { // Not UNKNOWN transaction type - transactionBodyWrapper.setField(field, transactionBody.build()); - } - - var transaction = transaction().build(); - // Clear these so that the builder can be reused and get new incremented values. - transactionBodyWrapper.clearTransactionID(); - - return blockItemBuilder + return BlockItem.builder() .transaction(transaction) .transactionResult(transactionResult) .transactionOutput(transactionOutputs) diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/reader/balance/CsvBalanceFileReaderTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/reader/balance/CsvBalanceFileReaderTest.java index 939acc89517..668a83d9415 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/reader/balance/CsvBalanceFileReaderTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/reader/balance/CsvBalanceFileReaderTest.java @@ -16,7 +16,7 @@ package com.hedera.mirror.importer.reader.balance; -import static com.hedera.mirror.common.domain.DigestAlgorithm.SHA_384; +import static com.hedera.mirror.common.util.DomainUtils.createSha384Digest; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -263,7 +263,7 @@ protected void assertAccountBalanceFile(AccountBalanceFile accountBalanceFile) { } protected void assertFileHash(File file, AccountBalanceFile accountBalanceFile) throws Exception { - MessageDigest md = MessageDigest.getInstance(SHA_384.getName()); + MessageDigest md = createSha384Digest(); byte[] array = Files.readAllBytes(file.toPath()); String fileHash = DomainUtils.bytesToHex(md.digest(array)); assertThat(accountBalanceFile.getFileHash()).isEqualTo(fileHash);