Skip to content

Commit b77116f

Browse files
authored
Add simple block node E2E tests (#11707)
- Add BlockNodeSimulator and BlockGenerator - Add a base integration test class and two simple test cases Signed-off-by: Xin Li <[email protected]>
1 parent a55c3fb commit b77116f

File tree

7 files changed

+454
-9
lines changed

7 files changed

+454
-9
lines changed

importer/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ dependencies {
5656
testImplementation("com.github.vertical-blank:sql-formatter")
5757
testImplementation("commons-beanutils:commons-beanutils")
5858
testImplementation("io.grpc:grpc-inprocess")
59+
testImplementation("io.grpc:grpc-netty")
5960
testImplementation("io.projectreactor:reactor-test")
6061
testImplementation("org.apache.commons:commons-math3")
6162
testImplementation("org.awaitility:awaitility")

importer/src/main/java/org/hiero/mirror/importer/reader/block/BlockRootHashDigest.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,14 @@
1010
import java.util.ArrayList;
1111
import java.util.List;
1212
import java.util.Objects;
13-
import lombok.Data;
1413
import org.hiero.mirror.common.util.DomainUtils;
1514

1615
/**
1716
* Calculates a block's root hash per the algorithm defined in HIP-1056. Note both the input merkle tree and the output
1817
* merkle tree are padded with SHA2-384 hash of an empty bytearray to be perfect binary trees. Note none of the methods
1918
* are reentrant.
2019
*/
21-
@Data
22-
class BlockRootHashDigest {
20+
public final class BlockRootHashDigest {
2321

2422
private static final byte[] EMPTY_HASH = createSha384Digest().digest(new byte[0]);
2523

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package org.hiero.mirror.importer.downloader.block;
4+
5+
import io.micrometer.core.instrument.MeterRegistry;
6+
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
7+
import jakarta.annotation.Resource;
8+
import java.util.List;
9+
import org.hiero.mirror.importer.ImporterIntegrationTest;
10+
import org.hiero.mirror.importer.downloader.CommonDownloaderProperties;
11+
import org.hiero.mirror.importer.downloader.StreamFileNotifier;
12+
import org.hiero.mirror.importer.reader.block.BlockStreamReader;
13+
import org.hiero.mirror.importer.repository.RecordFileRepository;
14+
import org.junit.jupiter.api.extension.ExtendWith;
15+
import org.mockito.Mock;
16+
import org.mockito.junit.jupiter.MockitoExtension;
17+
18+
@ExtendWith(MockitoExtension.class)
19+
abstract class AbstractBlockNodeIntegrationTest extends ImporterIntegrationTest {
20+
21+
@Mock(strictness = Mock.Strictness.LENIENT)
22+
protected StreamFileNotifier streamFileNotifier;
23+
24+
@Resource
25+
private BlockFileTransformer blockFileTransformer;
26+
27+
@Resource
28+
private BlockStreamReader blockStreamReader;
29+
30+
@Resource
31+
private CommonDownloaderProperties commonDownloaderProperties;
32+
33+
@Resource
34+
private ManagedChannelBuilderProvider managedChannelBuilderProvider;
35+
36+
@Resource
37+
private RecordFileRepository recordFileRepository;
38+
39+
private final ManagedChannelBuilderProvider inProcessManagedChannelBuilderProvider =
40+
new InProcessManagedChannelBuilderProvider();
41+
private final MeterRegistry meterRegistry = new SimpleMeterRegistry();
42+
43+
protected final BlockNodeSubscriber getBlockNodeSubscriber(List<BlockNodeProperties> nodes) {
44+
var blockProperties = new BlockProperties();
45+
blockProperties.setNodes(nodes);
46+
boolean isInProcess = nodes.getFirst().getPort() == -1;
47+
var blockStreamVerifier =
48+
new BlockStreamVerifier(blockFileTransformer, recordFileRepository, streamFileNotifier, meterRegistry);
49+
var channelBuilderProvider =
50+
isInProcess ? inProcessManagedChannelBuilderProvider : managedChannelBuilderProvider;
51+
return new BlockNodeSubscriber(
52+
blockStreamReader,
53+
blockStreamVerifier,
54+
commonDownloaderProperties,
55+
channelBuilderProvider,
56+
blockProperties);
57+
}
58+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package org.hiero.mirror.importer.downloader.block;
4+
5+
import static org.assertj.core.api.Assertions.assertThat;
6+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
7+
import static org.mockito.Mockito.times;
8+
import static org.mockito.Mockito.verify;
9+
10+
import java.util.List;
11+
import java.util.stream.LongStream;
12+
import org.hiero.mirror.common.domain.transaction.RecordFile;
13+
import org.hiero.mirror.importer.downloader.block.simulator.BlockGenerator;
14+
import org.hiero.mirror.importer.downloader.block.simulator.BlockNodeSimulator;
15+
import org.hiero.mirror.importer.exception.BlockStreamException;
16+
import org.hiero.mirror.importer.exception.InvalidStreamFileException;
17+
import org.junit.jupiter.api.AfterEach;
18+
import org.junit.jupiter.api.Test;
19+
import org.mockito.ArgumentCaptor;
20+
21+
final class SingleBlockNodeTest extends AbstractBlockNodeIntegrationTest {
22+
23+
private BlockNodeSimulator simulator;
24+
private BlockNodeSubscriber subscriber;
25+
26+
@AfterEach
27+
void cleanup() {
28+
if (subscriber != null) {
29+
subscriber.close();
30+
subscriber = null;
31+
}
32+
33+
if (simulator != null) {
34+
simulator.close();
35+
simulator = null;
36+
}
37+
}
38+
39+
@Test
40+
void multipleBlocks() {
41+
// given
42+
var generator = new BlockGenerator(0);
43+
simulator = new BlockNodeSimulator()
44+
.withChunksPerBlock(2)
45+
.withBlocks(generator.next(10))
46+
.start();
47+
subscriber = getBlockNodeSubscriber(List.of(simulator.toClientProperties()));
48+
49+
// when
50+
subscriber.get();
51+
52+
// then
53+
var captor = ArgumentCaptor.forClass(RecordFile.class);
54+
verify(streamFileNotifier, times(10)).verified(captor.capture());
55+
assertThat(captor.getAllValues())
56+
.map(RecordFile::getIndex)
57+
.containsExactlyElementsOf(LongStream.range(0, 10).boxed().toList());
58+
}
59+
60+
@Test
61+
void outOfOrder() {
62+
// given
63+
var generator = new BlockGenerator(0);
64+
simulator = new BlockNodeSimulator()
65+
.withBlocks(generator.next(10))
66+
.withHttpChannel()
67+
.withOutOrder()
68+
.start();
69+
subscriber = getBlockNodeSubscriber(List.of(simulator.toClientProperties()));
70+
71+
// when, then
72+
assertThatThrownBy(subscriber::get)
73+
.isInstanceOf(BlockStreamException.class)
74+
.hasCauseInstanceOf(InvalidStreamFileException.class);
75+
}
76+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package org.hiero.mirror.importer.downloader.block.simulator;
4+
5+
import com.hedera.hapi.block.stream.input.protoc.EventHeader;
6+
import com.hedera.hapi.block.stream.input.protoc.RoundHeader;
7+
import com.hedera.hapi.block.stream.output.protoc.BlockHeader;
8+
import com.hedera.hapi.block.stream.output.protoc.TransactionResult;
9+
import com.hedera.hapi.block.stream.protoc.BlockItem;
10+
import com.hedera.hapi.block.stream.protoc.BlockProof;
11+
import com.hedera.hapi.platform.event.legacy.EventTransaction;
12+
import com.hederahashgraph.api.proto.java.ResponseCodeEnum;
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
import lombok.SneakyThrows;
16+
import org.apache.commons.codec.binary.Hex;
17+
import org.hiero.block.api.protoc.BlockItemSet;
18+
import org.hiero.mirror.common.util.DomainUtils;
19+
import org.hiero.mirror.importer.parser.domain.RecordItemBuilder;
20+
import org.hiero.mirror.importer.reader.block.BlockRootHashDigest;
21+
22+
public final class BlockGenerator {
23+
private static final byte[] ALL_ZERO_HASH = new byte[48];
24+
25+
private final RecordItemBuilder recordItemBuilder = new RecordItemBuilder();
26+
27+
private int blockNumber;
28+
private byte[] previousBlockRootHash;
29+
30+
public BlockGenerator(int startBlockNumber) {
31+
blockNumber = startBlockNumber;
32+
if (blockNumber == 0) {
33+
previousBlockRootHash = ALL_ZERO_HASH;
34+
} else {
35+
previousBlockRootHash = recordItemBuilder.randomBytes(48);
36+
}
37+
}
38+
39+
public List<BlockItemSet> next(int count) {
40+
var blocks = new ArrayList<BlockItemSet>();
41+
for (int i = 0; i < count; i++) {
42+
blocks.add(next());
43+
}
44+
return blocks;
45+
}
46+
47+
@SneakyThrows
48+
private void calculateBlockRootHash(BlockItemSet block) {
49+
var blockRootHashDigest = new BlockRootHashDigest();
50+
blockRootHashDigest.setPreviousHash(previousBlockRootHash);
51+
blockRootHashDigest.setStartOfBlockStateHash(ALL_ZERO_HASH);
52+
53+
for (var blockItem : block.getBlockItemsList()) {
54+
switch (blockItem.getItemCase()) {
55+
case EVENT_HEADER, EVENT_TRANSACTION, ROUND_HEADER -> blockRootHashDigest.addInputBlockItem(blockItem);
56+
case BLOCK_HEADER, STATE_CHANGES, TRANSACTION_OUTPUT, TRANSACTION_RESULT ->
57+
blockRootHashDigest.addOutputBlockItem(blockItem);
58+
default -> {
59+
// other block items aren't considered input / output
60+
}
61+
}
62+
}
63+
64+
previousBlockRootHash = Hex.decodeHex(blockRootHashDigest.digest());
65+
}
66+
67+
private BlockItemSet next() {
68+
var builder = BlockItemSet.newBuilder();
69+
// block header
70+
builder.addBlockItems(BlockItem.newBuilder()
71+
.setBlockHeader(BlockHeader.newBuilder()
72+
.setBlockTimestamp(recordItemBuilder.timestamp())
73+
.setNumber(blockNumber)
74+
.build()));
75+
// round header
76+
builder.addBlockItems(BlockItem.newBuilder()
77+
.setRoundHeader(
78+
RoundHeader.newBuilder().setRoundNumber(blockNumber + 1).build()));
79+
// event header
80+
builder.addBlockItems(BlockItem.newBuilder().setEventHeader(EventHeader.getDefaultInstance()));
81+
82+
// transactions
83+
for (int i = 0; i < 10; i++) {
84+
builder.addAllBlockItems(transactionUnit());
85+
}
86+
87+
// block proof
88+
builder.addBlockItems(BlockItem.newBuilder()
89+
.setBlockProof(BlockProof.newBuilder()
90+
.setBlock(blockNumber)
91+
.setPreviousBlockRootHash(DomainUtils.fromBytes(previousBlockRootHash))
92+
.setStartOfBlockStateRootHash(DomainUtils.fromBytes(ALL_ZERO_HASH))));
93+
var block = builder.build();
94+
calculateBlockRootHash(block);
95+
blockNumber++;
96+
return block;
97+
}
98+
99+
private List<BlockItem> transactionUnit() {
100+
var recordItem = recordItemBuilder.cryptoTransfer().build();
101+
var eventTransaction = BlockItem.newBuilder()
102+
.setEventTransaction(EventTransaction.newBuilder()
103+
.setApplicationTransaction(recordItem.getTransaction().toByteString())
104+
.build())
105+
.build();
106+
var transactionResult = BlockItem.newBuilder()
107+
.setTransactionResult(TransactionResult.newBuilder()
108+
.setConsensusTimestamp(recordItem.getTransactionRecord().getConsensusTimestamp())
109+
.setStatus(ResponseCodeEnum.SUCCESS)
110+
.setTransferList(recordItem.getTransactionRecord().getTransferList())
111+
.build())
112+
.build();
113+
// for simplicity, no state changes
114+
return List.of(eventTransaction, transactionResult);
115+
}
116+
}

0 commit comments

Comments
 (0)