diff --git a/common/src/main/java/com/hedera/block/common/hasher/BlockMerkleTreeInfo.java b/common/src/main/java/com/hedera/block/common/hasher/BlockMerkleTreeInfo.java new file mode 100644 index 000000000..af05fbfce --- /dev/null +++ b/common/src/main/java/com/hedera/block/common/hasher/BlockMerkleTreeInfo.java @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +package com.hedera.block.common.hasher; + +import com.hedera.pbj.runtime.io.buffer.Bytes; +import java.util.List; + +public record BlockMerkleTreeInfo( + List> inputsMerkleTree, + List> outputsMerkleTree, + Bytes previousBlockHash, + Bytes stateRootHash, + Bytes blockHash) {} diff --git a/common/src/main/java/com/hedera/block/common/hasher/ConcurrentStreamingTreeHasher.java b/common/src/main/java/com/hedera/block/common/hasher/ConcurrentStreamingTreeHasher.java index 09b4c69eb..5f37a4fcb 100644 --- a/common/src/main/java/com/hedera/block/common/hasher/ConcurrentStreamingTreeHasher.java +++ b/common/src/main/java/com/hedera/block/common/hasher/ConcurrentStreamingTreeHasher.java @@ -10,6 +10,7 @@ import java.nio.ByteBuffer; import java.security.MessageDigest; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; @@ -56,6 +57,10 @@ public class ConcurrentStreamingTreeHasher implements StreamingTreeHasher { */ private boolean rootHashRequested = false; + private final CompletableFuture>> futureMerkleTree = new CompletableFuture<>(); + + private final List> merkleTree = new ArrayList<>(); + /** * Constructs a new {@link ConcurrentStreamingTreeHasher} with the given {@link ExecutorService}. * @@ -78,6 +83,7 @@ public ConcurrentStreamingTreeHasher( this.executorService = requireNonNull(executorService); this.hashCombineBatchSize = Preconditions.requireEven(hashCombineBatchSize, "Hash combine batch size must be an even number"); + merkleTree.add(new LinkedList<>()); ; } @@ -93,6 +99,7 @@ public void addLeaf(@NonNull final ByteBuffer hash) { numLeaves++; final byte[] bytes = new byte[HASH_LENGTH]; hash.get(bytes); + merkleTree.getFirst().add(Bytes.wrap(bytes)); // adding leafs as they come. combiner.combine(bytes); } @@ -103,6 +110,18 @@ public CompletableFuture rootHash() { return combiner.finalCombination(); } + /** + * Returns the complete Merkle tree as a list of levels (each level is a {@code List}). + * Level 0 contains the original leaves. + * The last level contains the root hash. + * + * @return a future that completes with the full Merkle tree + */ + @Override + public CompletableFuture>> merkleTree() { + return rootHash().thenCompose(ignore -> futureMerkleTree); + } + @Override public Status status() { if (numLeaves == 0) { @@ -176,6 +195,7 @@ public void combine(@NonNull final byte[] hash) { public CompletableFuture finalCombination() { if (height == rootHeight) { final byte[] rootHash = pendingHashes.isEmpty() ? EMPTY_HASHES[0] : pendingHashes.getFirst(); + completeMerkleTree(); return CompletableFuture.completedFuture(Bytes.wrap(rootHash)); } else { if (!pendingHashes.isEmpty()) { @@ -185,6 +205,19 @@ public CompletableFuture finalCombination() { } } + private void completeMerkleTree() { + + // pad right-most hashes with empty hashes if needed to keep the tree balanced + for(int i = 0; i < merkleTree.size(); i++){ + int leaves = merkleTree.get(i).size(); + if(leaves % 2 != 0){ + merkleTree.get(i).add(Bytes.wrap(EMPTY_HASHES[i])); + } + } + + futureMerkleTree.complete(merkleTree); + } + public void flushAvailable(@NonNull final List rightmostHashes, final int stopHeight) { if (height < stopHeight) { final byte[] newPendingHash = pendingHashes.size() % 2 == 0 ? null : pendingHashes.removeLast(); @@ -212,12 +245,20 @@ private void schedulePendingWork() { pendingCombination = CompletableFuture.supplyAsync(() -> combine(hashes), executorService); } combination = combination.thenCombine(pendingCombination, (ignore, combined) -> { + appendLevelToMerkleTree(combined); combined.forEach(delegate::combine); return null; }); pendingHashes = new ArrayList<>(); } + private void appendLevelToMerkleTree(List combined){ + if(merkleTree.size() <= height + 1) { + merkleTree.add(new LinkedList<>()); + } + merkleTree.get(height + 1).addAll(combined.stream().map(Bytes::wrap).toList()); + } + private List combine(@NonNull final List hashes) { final List result = new ArrayList<>(); final MessageDigest digest = DIGESTS.get(); diff --git a/common/src/main/java/com/hedera/block/common/hasher/MerkleProofCalculator.java b/common/src/main/java/com/hedera/block/common/hasher/MerkleProofCalculator.java new file mode 100644 index 000000000..2ef7ee192 --- /dev/null +++ b/common/src/main/java/com/hedera/block/common/hasher/MerkleProofCalculator.java @@ -0,0 +1,130 @@ +package com.hedera.block.common.hasher; + +import com.hedera.pbj.runtime.io.buffer.Bytes; + +import java.util.ArrayList; +import java.util.List; + +public class MerkleProofCalculator { + + private MerkleProofCalculator() { + throw new UnsupportedOperationException("Utility Class"); + } + + /** + * Find the index of a byte array in a list of byte arrays. + * @param list list of hashes as Bytes + * @param target leaf to find index within the list + * @return index of the target leaf in the list, or -1 if not found + */ + public static int indexOfByteArray(List list, Bytes target) { + for (int i = 0; i < list.size(); i++) { + if (list.get(i).equals(target)) { + return i; + } + } + return -1; + } + + /** + * Calculate the Merkle proof for a leaf hash in a block Merkle tree + * @param blockMerkleTreeInfo block Merkle tree information + * @param leafHash leaf hash to calculate proof for + * @return list of MerkleProofElement representing the proof, if item is not found within block, return null + */ + public static List calculateBlockMerkleProof(BlockMerkleTreeInfo blockMerkleTreeInfo, Bytes leafHash) { + + // is the leaf in the input tree + // all leaf hashes are at the lowest level = 0 + int leafIndex = indexOfByteArray(blockMerkleTreeInfo.inputsMerkleTree().getFirst(), leafHash); + boolean isInputLeaf = leafIndex != -1; + // is the leaf in the output tree + if(leafIndex == -1) { + leafIndex = indexOfByteArray(blockMerkleTreeInfo.outputsMerkleTree().getFirst(), leafHash); + isInputLeaf = false; + } + + // if there is no match + if(leafIndex == -1) { + return null; + } + + // if there is a match + List proof = new ArrayList<>(); + // get proof elements up to the root of the subtree + if(isInputLeaf) { + proof.addAll(calculateMerkleProof(blockMerkleTreeInfo.inputsMerkleTree(), leafIndex)); + //proof.add(new MerkleProofElement(blockMerkleTreeInfo.outputsMerkleTree().getLast().getFirst(), false)); + } else { + proof.addAll(calculateMerkleProof(blockMerkleTreeInfo.outputsMerkleTree(), leafIndex)); + //proof.add(new MerkleProofElement(blockMerkleTreeInfo.inputsMerkleTree().getLast().getFirst(), false)); + } + + // get proof elements from the root of the subtree to the root of the blockRootHash + // the last levels of the tree are like this: + // leftSide: combine(previousBlockHash, inputsRootHash) + // rightSide: combine(outputsRootHash, stateRootHash) + if(isInputLeaf) { + proof.add(new MerkleProofElement(blockMerkleTreeInfo.previousBlockHash(), true)); + Bytes sibling = HashingUtilities.combine(blockMerkleTreeInfo.outputsMerkleTree().getLast().getFirst(), blockMerkleTreeInfo.stateRootHash()); + proof.add(new MerkleProofElement(sibling, false)); + } else { + proof.add(new MerkleProofElement(blockMerkleTreeInfo.stateRootHash(), false)); + Bytes sibling = HashingUtilities.combine(blockMerkleTreeInfo.previousBlockHash(), blockMerkleTreeInfo.inputsMerkleTree().getLast().getFirst()); + proof.add(new MerkleProofElement(sibling, true)); + } + + return new ArrayList<>(proof); + } + + /** + * Calculate the Merkle root hash of a list of leaf hashes. + * Requires the completeMerkleTree to be a list of lists of byte arrays, where each list represents a level of the tree, fully padded. + * + * @param completeMerkleTree A list of lists of byte arrays, where each list represents a level of the tree, fully padded. + * @return The Merkle root hash. + */ + public static List calculateMerkleProof(List> completeMerkleTree, int leafIndex) { + List proof = new ArrayList<>(); + int index = leafIndex; + + // Iterate over each level except the root. + for(int level = 0; level < completeMerkleTree.size() - 1; level++) { + List levelHashes = completeMerkleTree.get(level); + if(index % 2 == 0) { + // If the index is even, the sibling is the next hash in the list. + proof.add(new MerkleProofElement(levelHashes.get(index + 1), false)); + } else { + // If the index is odd, the sibling is the previous hash in the list. + proof.add(new MerkleProofElement(levelHashes.get(index - 1), true)); + } + // Move up to the parent level. + index /= 2; + } + + return proof; + } + + /** + * Verifies a Merkle proof for a given leaf hash. + * + * @param proof A list of MerkleProofElement that include sibling hashes and whether they're on the left. + * @param leafHash The hash (as a byte array) of the leaf node. + * @param rootHash The expected Merkle root hash. + * @return true if the proof is valid and the computed root matches rootHash; false otherwise. + */ + public static boolean verifyMerkleProof(List proof, Bytes leafHash, Bytes rootHash) { + Bytes computedHash = leafHash; + for (MerkleProofElement element : proof) { + if (element.isLeft()) { + // Sibling is on the left: concatenate sibling hash + computed hash. + computedHash = HashingUtilities.combine(element.hash(), computedHash); + } else { + // Sibling is on the right: concatenate computed hash + sibling hash. + computedHash = HashingUtilities.combine(computedHash, element.hash()); + } + } + return computedHash.equals(rootHash); + } + +} diff --git a/common/src/main/java/com/hedera/block/common/hasher/MerkleProofElement.java b/common/src/main/java/com/hedera/block/common/hasher/MerkleProofElement.java new file mode 100644 index 000000000..f655a420d --- /dev/null +++ b/common/src/main/java/com/hedera/block/common/hasher/MerkleProofElement.java @@ -0,0 +1,5 @@ +package com.hedera.block.common.hasher; + +import com.hedera.pbj.runtime.io.buffer.Bytes; + +public record MerkleProofElement(Bytes hash, boolean isLeft) { } diff --git a/common/src/main/java/com/hedera/block/common/hasher/NaiveStreamingTreeHasher.java b/common/src/main/java/com/hedera/block/common/hasher/NaiveStreamingTreeHasher.java index 33844edf1..ee8bb99dd 100644 --- a/common/src/main/java/com/hedera/block/common/hasher/NaiveStreamingTreeHasher.java +++ b/common/src/main/java/com/hedera/block/common/hasher/NaiveStreamingTreeHasher.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Queue; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; /** * A naive implementation of {@link StreamingTreeHasher} that computes the root hash of a perfect binary Merkle tree of @@ -22,6 +23,7 @@ public class NaiveStreamingTreeHasher implements StreamingTreeHasher { private final List leafHashes = new ArrayList<>(); private boolean rootHashRequested = false; + private final CompletableFuture>> futureMerkleTree = new CompletableFuture<>(); /** * Constructor for the {@link NaiveStreamingTreeHasher}. @@ -55,7 +57,9 @@ public CompletableFuture rootHash() { hashes.add(EMPTY_HASH); } } + final List> completeMerkleTree = new LinkedList<>(); while (hashes.size() > 1) { + completeMerkleTree.add(new ArrayList<>(hashes.stream().map(Bytes::wrap).toList())); final Queue newLeafHashes = new LinkedList<>(); while (!hashes.isEmpty()) { final byte[] left = hashes.poll(); @@ -67,6 +71,12 @@ public CompletableFuture rootHash() { } hashes = newLeafHashes; } + completeMerkleTree.add(new ArrayList<>(hashes.stream().map(Bytes::wrap).toList())); + futureMerkleTree.complete(completeMerkleTree); return CompletableFuture.completedFuture(Bytes.wrap(requireNonNull(hashes.poll()))); } + + public CompletableFuture>> merkleTree() { + return rootHash().thenCompose(ignore -> futureMerkleTree); + } } diff --git a/common/src/main/java/com/hedera/block/common/hasher/StreamingTreeHasher.java b/common/src/main/java/com/hedera/block/common/hasher/StreamingTreeHasher.java index db573bbcc..4b0ac0bf1 100644 --- a/common/src/main/java/com/hedera/block/common/hasher/StreamingTreeHasher.java +++ b/common/src/main/java/com/hedera/block/common/hasher/StreamingTreeHasher.java @@ -52,6 +52,8 @@ public boolean isEmpty() { */ CompletableFuture rootHash(); + CompletableFuture>> merkleTree(); + /** * If supported, blocks until this hasher can give a deterministic summary of the status of the * tree hash computation. diff --git a/common/src/test/java/com/hedera/block/common/hasher/MerkleProofCalculatorTest.java b/common/src/test/java/com/hedera/block/common/hasher/MerkleProofCalculatorTest.java new file mode 100644 index 000000000..48713fded --- /dev/null +++ b/common/src/test/java/com/hedera/block/common/hasher/MerkleProofCalculatorTest.java @@ -0,0 +1,99 @@ +package com.hedera.block.common.hasher; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.hedera.pbj.runtime.io.buffer.Bytes; +import org.junit.jupiter.api.Test; + +public class MerkleProofCalculatorTest { + + @Test + public void testCalculateMerkleProof() { + // Build a simple Merkle tree: + // Level 0 (leaves): [L0, L1, L2, L3] + // Level 1 (internal nodes): [H0_1, H2_3] + // Level 2 (root): [H0_3] + // + // In this precomputed tree, assume: + // - H0_1 is the hash of L0 and L1. + // - H2_3 is the hash of L2 and L3. + // - H0_3 is the hash of H0_1 and H2_3. + // For our test, we use literal byte arrays with the given names. + + Bytes L0 = Bytes.wrap("L0".getBytes(StandardCharsets.UTF_8)); + Bytes L1 = Bytes.wrap("L1".getBytes(StandardCharsets.UTF_8)); + Bytes L2 = Bytes.wrap("L2".getBytes(StandardCharsets.UTF_8)); + Bytes L3 = Bytes.wrap("L3".getBytes(StandardCharsets.UTF_8)); + Bytes H0_1 = Bytes.wrap("H0_1".getBytes(StandardCharsets.UTF_8)); + Bytes H2_3 = Bytes.wrap("H2_3".getBytes(StandardCharsets.UTF_8)); + Bytes H0_3 = Bytes.wrap("H0_3".getBytes(StandardCharsets.UTF_8)); + + List> completeMerkleTree = new ArrayList<>(); + + // Level 0: Leaves + completeMerkleTree.add(Arrays.asList(L0, L1, L2, L3)); + // Level 1: Intermediate nodes + completeMerkleTree.add(Arrays.asList(H0_1, H2_3)); + // Level 2: Root + completeMerkleTree.add(Arrays.asList(H0_3)); + + MerkleProofCalculator calculator = new MerkleProofCalculator(); + + // For this test, we choose leaf index 2 (which corresponds to L2). + // Expected proof: + // - At Level 0: sibling of index 2 is at index 3 (L3). + // - At Level 1: then parent's index becomes 2/2 = 1; its sibling is at index 0 (H0_1). + // So the expected proof is [L3, H0_1]. + int leafIndex = 2; + List proof = calculator.calculateMerkleProof(completeMerkleTree, leafIndex); + + List expectedProof = new ArrayList<>(); + //expectedProof.add(L3); + expectedProof.add(new MerkleProofElement(L3, false)); + + //expectedProof.add(H0_1); + expectedProof.add(new MerkleProofElement(H0_1, true)); + + // Check that the proof has the expected size. + assertEquals(expectedProof.size(), proof.size(), "Proof size does not match expected size."); + + // Verify that each element in the proof matches the expected value. + for (int i = 0; i < expectedProof.size(); i++) { + MerkleProofElement expectedElement = expectedProof.get(i); + MerkleProofElement actualElement = proof.get(i); + + assertArrayEquals(expectedElement.hash().toByteArray(), actualElement.hash().toByteArray(), "Hashes do not match."); + assertEquals(expectedElement.isLeft(), actualElement.isLeft(), "isLeft does not match."); + } + } + + @Test + public void testIndexIfExist() { + // Prepare some sample leaf hashes. + byte[] leaf1 = "leaf1".getBytes(StandardCharsets.UTF_8); + byte[] leaf2 = "leaf2".getBytes(StandardCharsets.UTF_8); + byte[] leaf3 = "leaf3".getBytes(StandardCharsets.UTF_8); + + List leafHashes = new ArrayList<>(); + leafHashes.add(leaf1); + leafHashes.add(leaf2); + leafHashes.add(leaf3); + + MerkleProofCalculator calculator = new MerkleProofCalculator(); + + // Test finding an existing element. + int foundIndex = calculator.indexIfExist(leafHashes, leaf2); + assertEquals(1, foundIndex, "The index of leaf2 should be 1."); + + // Test with an element that does not exist. + byte[] nonExistingLeaf = "nonExisting".getBytes(StandardCharsets.UTF_8); + int notFoundIndex = calculator.indexIfExist(leafHashes, nonExistingLeaf); + assertEquals(-1, notFoundIndex, "A non-existing leaf should return an index of -1."); + } +} diff --git a/server/src/main/java/com/hedera/block/server/verification/VerificationConfig.java b/server/src/main/java/com/hedera/block/server/verification/VerificationConfig.java index 188c3ded7..0f5ce51d7 100644 --- a/server/src/main/java/com/hedera/block/server/verification/VerificationConfig.java +++ b/server/src/main/java/com/hedera/block/server/verification/VerificationConfig.java @@ -17,7 +17,7 @@ @ConfigData("verification") public record VerificationConfig( @Loggable @ConfigProperty(defaultValue = "PRODUCTION") VerificationServiceType type, - @Loggable @ConfigProperty(defaultValue = "ASYNC") BlockVerificationSessionType sessionType, + @Loggable @ConfigProperty(defaultValue = "SYNC") BlockVerificationSessionType sessionType, @Loggable @ConfigProperty(defaultValue = "32") int hashCombineBatchSize) { /** diff --git a/server/src/main/java/com/hedera/block/server/verification/VerificationResult.java b/server/src/main/java/com/hedera/block/server/verification/VerificationResult.java index 8327c69aa..8129efa30 100644 --- a/server/src/main/java/com/hedera/block/server/verification/VerificationResult.java +++ b/server/src/main/java/com/hedera/block/server/verification/VerificationResult.java @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package com.hedera.block.server.verification; +import com.hedera.block.common.hasher.BlockMerkleTreeInfo; import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; @@ -11,4 +12,8 @@ * @param blockHash the block hash * @param status the verification status */ -public record VerificationResult(long blockNumber, @NonNull Bytes blockHash, @NonNull BlockVerificationStatus status) {} +public record VerificationResult( + long blockNumber, + @NonNull Bytes blockHash, + @NonNull BlockVerificationStatus status, + BlockMerkleTreeInfo blockMerkleTreeInfo) {} diff --git a/server/src/main/java/com/hedera/block/server/verification/session/BlockVerificationSessionBase.java b/server/src/main/java/com/hedera/block/server/verification/session/BlockVerificationSessionBase.java index 4d15117cf..019677b2f 100644 --- a/server/src/main/java/com/hedera/block/server/verification/session/BlockVerificationSessionBase.java +++ b/server/src/main/java/com/hedera/block/server/verification/session/BlockVerificationSessionBase.java @@ -3,6 +3,7 @@ import static java.lang.System.Logger.Level.INFO; +import com.hedera.block.common.hasher.BlockMerkleTreeInfo; import com.hedera.block.common.hasher.Hashes; import com.hedera.block.common.hasher.HashingUtilities; import com.hedera.block.common.hasher.StreamingTreeHasher; @@ -134,6 +135,12 @@ protected void finalizeVerification(BlockProof blockProof) { Bytes blockHash = HashingUtilities.computeFinalBlockHash(blockProof, inputTreeHasher, outputTreeHasher); VerificationResult result; boolean verified = signatureVerifier.verifySignature(blockHash, blockProof.blockSignature()); + BlockMerkleTreeInfo blockMerkleTreeInfo = new BlockMerkleTreeInfo( + inputTreeHasher.merkleTree().join(), + outputTreeHasher.merkleTree().join(), + blockProof.previousBlockRootHash(), + blockProof.startOfBlockStateRootHash(), + blockHash); if (verified) { long verificationLatency = System.nanoTime() - blockWorkStartTime; metricsService @@ -143,14 +150,16 @@ protected void finalizeVerification(BlockProof blockProof) { .get(BlockNodeMetricTypes.Counter.VerificationBlocksVerified) .increment(); - result = new VerificationResult(blockNumber, blockHash, BlockVerificationStatus.VERIFIED); + result = new VerificationResult( + blockNumber, blockHash, BlockVerificationStatus.VERIFIED, blockMerkleTreeInfo); } else { LOGGER.log(INFO, "Block verification failed for block number: {0}", blockNumber); metricsService .get(BlockNodeMetricTypes.Counter.VerificationBlocksFailed) .increment(); - result = new VerificationResult(blockNumber, blockHash, BlockVerificationStatus.INVALID_HASH_OR_SIGNATURE); + result = new VerificationResult( + blockNumber, blockHash, BlockVerificationStatus.INVALID_HASH_OR_SIGNATURE, blockMerkleTreeInfo); } shutdownSession(); verificationResultFuture.complete(result); diff --git a/server/src/test/java/com/hedera/block/server/verification/BlockVerificationServiceImplTest.java b/server/src/test/java/com/hedera/block/server/verification/BlockVerificationServiceImplTest.java index dc691deb9..3ff046dbc 100644 --- a/server/src/test/java/com/hedera/block/server/verification/BlockVerificationServiceImplTest.java +++ b/server/src/test/java/com/hedera/block/server/verification/BlockVerificationServiceImplTest.java @@ -93,11 +93,6 @@ void testOnBlockItemsReceivedNoBlockHeaderWithCurrentSession() throws ParseExcep verifyNoInteractions(verificationBlocksReceived, verificationBlocksFailed); } - private VerificationResult getVerificationResult(long blockNumber) { - return new VerificationResult( - blockNumber, Bytes.wrap(("hash" + blockNumber).getBytes()), BlockVerificationStatus.VERIFIED); - } - private BlockHeader getBlockHeader(long blockNumber) { long previousBlockNumber = blockNumber - 1; diff --git a/server/src/test/java/com/hedera/block/server/verification/session/BlockVerificationSessionBaseTest.java b/server/src/test/java/com/hedera/block/server/verification/session/BlockVerificationSessionBaseTest.java index 8e8521c48..617885ec6 100644 --- a/server/src/test/java/com/hedera/block/server/verification/session/BlockVerificationSessionBaseTest.java +++ b/server/src/test/java/com/hedera/block/server/verification/session/BlockVerificationSessionBaseTest.java @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package com.hedera.block.server.verification.session; +import static com.hedera.block.common.hasher.HashingUtilities.getBlockItemHash; import static com.hedera.block.common.utils.FileUtilities.readGzipFileUnsafe; import static com.hedera.block.server.metrics.BlockNodeMetricTypes.Counter.VerificationBlockTime; import static com.hedera.block.server.metrics.BlockNodeMetricTypes.Counter.VerificationBlocksError; @@ -16,6 +17,8 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import com.hedera.block.common.hasher.MerkleProofCalculator; +import com.hedera.block.common.hasher.MerkleProofElement; import com.hedera.block.server.metrics.MetricsService; import com.hedera.block.server.verification.BlockVerificationStatus; import com.hedera.block.server.verification.VerificationResult; @@ -32,6 +35,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -103,6 +107,21 @@ void testSuccessfulVerification() throws Exception { verify(verificationBlocksVerified, times(1)).increment(); verify(verificationBlockTime, times(1)).add(any(Long.class)); verifyNoMoreInteractions(verificationBlocksFailed); + + // lets get the first transaction result + BlockItemUnparsed blockItem = blockItems.stream() + .filter(item -> item.hasStateChanges()) + .collect(Collectors.toList()) + .get(0); + Bytes blockItemHash = Bytes.wrap(getBlockItemHash(blockItem).array()); + + // get proof for the item + List proof = + MerkleProofCalculator.calculateBlockMerkleProof(result.blockMerkleTreeInfo(), blockItemHash); + + // verify that the proof is valid + boolean isBlockItemVerified = MerkleProofCalculator.verifyMerkleProof(proof, blockItemHash, result.blockHash()); + assertTrue(isBlockItemVerified, "Block item proof is not valid"); } @Test