Skip to content

Commit

Permalink
Adding UT Coverage for BlockVerificationSession related classes.
Browse files Browse the repository at this point in the history
Small improvements in other tests as well.
Improvements to the flow of finalizeVerification thanks to issues surfaced during unit testing

Signed-off-by: Alfredo Gutierrez <[email protected]>
  • Loading branch information
AlfredoG87 committed Dec 18, 2024
1 parent fa09df7 commit c56d8bf
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 211 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -121,28 +121,28 @@ protected void processBlockItems(List<BlockItemUnparsed> blockItems) throws Pars
*/
protected void finalizeVerification(BlockProof blockProof) {
Bytes blockHash = CommonUtils.computeFinalBlockHash(blockProof, inputTreeHasher, outputTreeHasher);

VerificationResult result;
boolean verified = signatureVerifier.verifySignature(blockHash, blockProof.blockSignature());
if (verified) {
verificationResultFuture.complete(
new VerificationResult(blockNumber, blockHash, BlockVerificationStatus.VERIFIED));
long verificationLatency = System.nanoTime() - blockWorkStartTime;
metricsService
.get(BlockNodeMetricTypes.Counter.VerificationBlockTime)
.add(verificationLatency);
metricsService
.get(BlockNodeMetricTypes.Counter.VerificationBlocksVerified)
.increment();

result = new VerificationResult(blockNumber, blockHash, BlockVerificationStatus.VERIFIED);
} else {
LOGGER.log(INFO, "Block verification failed for block number: {0}", blockNumber);
metricsService
.get(BlockNodeMetricTypes.Counter.VerificationBlocksFailed)
.increment();
verificationResultFuture.complete(
new VerificationResult(blockNumber, blockHash, BlockVerificationStatus.SIGNATURE_INVALID));
}

result = new VerificationResult(blockNumber, blockHash, BlockVerificationStatus.SIGNATURE_INVALID);
}
shutdownSession();
verificationResultFuture.complete(result);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ public BlockVerificationSession createSession(@NonNull final BlockHeader blockHe
case ASYNC -> new BlockVerificationSessionAsync(
blockHeader, metricsService, signatureVerifier, executorService, hashCombineBatchSize);
case SYNC -> new BlockVerificationSessionSync(blockHeader, metricsService, signatureVerifier);
default -> throw new IllegalArgumentException("Unsupported session type: " + type);
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@
import java.util.SplittableRandom;
import java.util.concurrent.ForkJoinPool;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class ConcurrentStreamingTreeHasherTest {
private static final SplittableRandom RANDOM = new SplittableRandom();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
* 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.block.server.verification.session;

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;
import static com.hedera.block.server.metrics.BlockNodeMetricTypes.Counter.VerificationBlocksFailed;
import static com.hedera.block.server.metrics.BlockNodeMetricTypes.Counter.VerificationBlocksVerified;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import com.hedera.block.server.metrics.MetricsService;
import com.hedera.block.server.verification.BlockVerificationStatus;
import com.hedera.block.server.verification.VerificationResult;
import com.hedera.block.server.verification.signature.SignatureVerifier;
import com.hedera.hapi.block.BlockItemUnparsed;
import com.hedera.hapi.block.BlockUnparsed;
import com.hedera.hapi.block.stream.output.BlockHeader;
import com.hedera.pbj.runtime.ParseException;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import com.swirlds.metrics.api.Counter;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public abstract class AbstractBlockVerificationSessionTest {

@Mock
protected MetricsService metricsService;

@Mock
protected SignatureVerifier signatureVerifier;

@Mock
protected Counter verificationBlocksVerified;

@Mock
protected Counter verificationBlocksFailed;

@Mock
protected Counter verificationBlockTime;

@Mock
protected Counter verificationBlocksError;

protected final Bytes hashing01BlockHash = Bytes.fromHex(
"006ae77f87ff57df598f4d6536dcb5c0a5c1f840c2fef817b2faebd554d32cfc9a4eaee1d873ed88de668b53b7839117");

@BeforeEach
void setUpBase() {
MockitoAnnotations.openMocks(this);
when(metricsService.get(VerificationBlocksVerified)).thenReturn(verificationBlocksVerified);
when(metricsService.get(VerificationBlocksFailed)).thenReturn(verificationBlocksFailed);
when(metricsService.get(VerificationBlockTime)).thenReturn(verificationBlockTime);
when(metricsService.get(VerificationBlocksError)).thenReturn(verificationBlocksError);
}

protected abstract BlockVerificationSession createSession(BlockHeader blockHeader);

protected List<BlockItemUnparsed> getTestBlock1Items() throws IOException, ParseException, URISyntaxException {
Path block01Path =
Path.of(getClass().getResource("/test-blocks/hashing-01.blk.gz").toURI());
Bytes block01Bytes = Bytes.wrap(readGzipFileUnsafe(block01Path));
BlockUnparsed blockUnparsed = BlockUnparsed.PROTOBUF.parse(block01Bytes);
return blockUnparsed.blockItems();
}

@Test
void testSuccessfulVerification() throws Exception {
// Given
List<BlockItemUnparsed> blockItems = getTestBlock1Items();
BlockHeader blockHeader =
BlockHeader.PROTOBUF.parse(blockItems.getFirst().blockHeader());
BlockVerificationSession session = createSession(blockHeader);

when(signatureVerifier.verifySignature(any(Bytes.class), any(Bytes.class)))
.thenReturn(true);

// When
session.appendBlockItems(blockItems);
CompletableFuture<VerificationResult> future = session.getVerificationResult();
VerificationResult result = future.get();

// Then
assertEquals(BlockVerificationStatus.VERIFIED, result.status());
assertEquals(1L, result.blockNumber());
assertEquals(hashing01BlockHash, result.blockHash());
assertFalse(session.isRunning());
verify(verificationBlocksVerified, times(1)).increment();
verify(verificationBlockTime, times(1)).add(any(Long.class));
verifyNoMoreInteractions(verificationBlocksFailed);
}

@Test
void testSuccessfulVerification_multipleAppends() throws Exception {
// Given
List<BlockItemUnparsed> blockItems = getTestBlock1Items();
// Slice list into 2 parts of different sizes
List<BlockItemUnparsed> blockItems1 = blockItems.subList(0, 3);
List<BlockItemUnparsed> blockItems2 = blockItems.subList(3, blockItems.size());
BlockHeader blockHeader =
BlockHeader.PROTOBUF.parse(blockItems.getFirst().blockHeader());
BlockVerificationSession session = createSession(blockHeader);
when(signatureVerifier.verifySignature(any(Bytes.class), any(Bytes.class)))
.thenReturn(true);

// When
session.appendBlockItems(blockItems1);
session.appendBlockItems(blockItems2);
CompletableFuture<VerificationResult> future = session.getVerificationResult();
VerificationResult result = future.get();

// Then
assertEquals(BlockVerificationStatus.VERIFIED, result.status());
assertEquals(1L, result.blockNumber());
assertEquals(hashing01BlockHash, result.blockHash());
assertFalse(session.isRunning());
verify(verificationBlocksVerified, times(1)).increment();
verify(verificationBlockTime, times(1)).add(any(Long.class));
verifyNoMoreInteractions(verificationBlocksFailed);
}

@Test
void testVerificationFailure() throws Exception {
// Given
List<BlockItemUnparsed> blockItems = getTestBlock1Items();
final Bytes hashing01BlockHash = Bytes.fromHex(
"006ae77f87ff57df598f4d6536dcb5c0a5c1f840c2fef817b2faebd554d32cfc9a4eaee1d873ed88de668b53b7839117");
BlockHeader blockHeader =
BlockHeader.PROTOBUF.parse(blockItems.getFirst().blockHeader());
BlockVerificationSession session = createSession(blockHeader);
when(signatureVerifier.verifySignature(any(Bytes.class), any(Bytes.class)))
.thenReturn(false);

// When
session.appendBlockItems(blockItems);
CompletableFuture<VerificationResult> future = session.getVerificationResult();
VerificationResult result = future.get();

// Then
assertEquals(BlockVerificationStatus.SIGNATURE_INVALID, result.status());
assertEquals(1L, result.blockNumber());
assertEquals(hashing01BlockHash, result.blockHash());
assertFalse(session.isRunning());
verifyNoMoreInteractions(verificationBlocksVerified);
verify(verificationBlocksFailed, times(1)).increment();
}

@Test
void testAppendBlockItemsNotRunning() throws Exception {
// Given
List<BlockItemUnparsed> blockItems = getTestBlock1Items();
BlockHeader blockHeader =
BlockHeader.PROTOBUF.parse(blockItems.getFirst().blockHeader());
BlockVerificationSession session = createSession(blockHeader);
when(signatureVerifier.verifySignature(any(Bytes.class), any(Bytes.class)))
.thenReturn(true);
// send a whole block and wait for the result, the session should be completed.
session.appendBlockItems(blockItems);
CompletableFuture<VerificationResult> future = session.getVerificationResult();
VerificationResult result = future.get();

// metrics should be 1
verify(verificationBlocksVerified, times(1)).increment();
verify(verificationBlockTime, times(1)).add(any(Long.class));

// When
// Try to append more items after the session has completed
session.appendBlockItems(blockItems);

// Then
// counters should still be 1
verify(verificationBlocksVerified, times(1)).increment();
verify(verificationBlockTime, times(1)).add(any(Long.class));
}

@Test
void testParseException()
throws IOException, ParseException, URISyntaxException, ExecutionException, InterruptedException {
// Given
List<BlockItemUnparsed> blockItems = getTestBlock1Items();
BlockHeader blockHeader =
BlockHeader.PROTOBUF.parse(blockItems.getFirst().blockHeader());
blockItems.set(
blockItems.size() - 1,
BlockItemUnparsed.newBuilder().blockProof(Bytes.wrap("invalid")).build());
BlockVerificationSession session = createSession(blockHeader);

// When
session.appendBlockItems(blockItems);
CompletableFuture<VerificationResult> future = session.getVerificationResult();
assertThrows(ExecutionException.class, future::get);

// Then
assertTrue(future.isCompletedExceptionally());
verify(verificationBlocksError, times(1)).increment();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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.block.server.verification.session;

import com.hedera.hapi.block.stream.output.BlockHeader;
import java.util.concurrent.Executors;

class BlockVerificationSessionAsyncTest extends AbstractBlockVerificationSessionTest {

@Override
protected BlockVerificationSession createSession(BlockHeader blockHeader) {
return new BlockVerificationSessionAsync(
blockHeader, metricsService, signatureVerifier, Executors.newSingleThreadExecutor(), 32);
}
}
Loading

0 comments on commit c56d8bf

Please sign in to comment.