diff --git a/util/build.gradle b/util/build.gradle index c0d32361e..ef72a209b 100644 --- a/util/build.gradle +++ b/util/build.gradle @@ -39,6 +39,7 @@ dependencies { implementation 'org.apache.commons:commons-lang3' implementation 'org.apache.logging.log4j:log4j-core' implementation 'org.apache.logging.log4j:log4j-slf4j2-impl' + implementation 'org.bouncycastle:bcpkix-jdk18on' implementation 'org.xerial.snappy:snappy-java' testImplementation 'org.mockito:mockito-junit-jupiter' diff --git a/util/src/main/java/org/hyperledger/besu/util/era1/Era1BlockIndex.java b/util/src/main/java/org/hyperledger/besu/util/era1/Era1BlockIndex.java new file mode 100644 index 000000000..eed5e981b --- /dev/null +++ b/util/src/main/java/org/hyperledger/besu/util/era1/Era1BlockIndex.java @@ -0,0 +1,25 @@ +/* + * Copyright contributors to Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.util.era1; + +import java.util.List; + +/** + * Represents a block index in an era1 file + * + * @param startingBlockIndex The first blockIndex number indexed by this block index + * @param indexes The indexes of the blocks indexed by this block index + */ +public record Era1BlockIndex(long startingBlockIndex, List indexes) {} diff --git a/util/src/main/java/org/hyperledger/besu/util/era1/Era1ExecutionBlockBody.java b/util/src/main/java/org/hyperledger/besu/util/era1/Era1ExecutionBlockBody.java new file mode 100644 index 000000000..266be392f --- /dev/null +++ b/util/src/main/java/org/hyperledger/besu/util/era1/Era1ExecutionBlockBody.java @@ -0,0 +1,23 @@ +/* + * Copyright contributors to Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.util.era1; + +/** + * Represents an execution block body in an era1 file + * + * @param block The execution block + * @param blockIndex The blockIndex number + */ +public record Era1ExecutionBlockBody(byte[] block, int blockIndex) {} diff --git a/util/src/main/java/org/hyperledger/besu/util/era1/Era1ExecutionBlockHeader.java b/util/src/main/java/org/hyperledger/besu/util/era1/Era1ExecutionBlockHeader.java new file mode 100644 index 000000000..0a5f32f5b --- /dev/null +++ b/util/src/main/java/org/hyperledger/besu/util/era1/Era1ExecutionBlockHeader.java @@ -0,0 +1,23 @@ +/* + * Copyright contributors to Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.util.era1; + +/** + * Represents an execution block header in an era1 file + * + * @param header The execution block header + * @param blockIndex The blockIndex number + */ +public record Era1ExecutionBlockHeader(byte[] header, int blockIndex) {} diff --git a/util/src/main/java/org/hyperledger/besu/util/era1/Era1ExecutionBlockReceipts.java b/util/src/main/java/org/hyperledger/besu/util/era1/Era1ExecutionBlockReceipts.java new file mode 100644 index 000000000..d23dc222a --- /dev/null +++ b/util/src/main/java/org/hyperledger/besu/util/era1/Era1ExecutionBlockReceipts.java @@ -0,0 +1,23 @@ +/* + * Copyright contributors to Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.util.era1; + +/** + * Represents an execution block's transaction receipts in an era1 file + * + * @param receipts The execution block's transaction receipts + * @param blockIndex The blockIndex number + */ +public record Era1ExecutionBlockReceipts(byte[] receipts, int blockIndex) {} diff --git a/util/src/main/java/org/hyperledger/besu/util/era1/Era1Reader.java b/util/src/main/java/org/hyperledger/besu/util/era1/Era1Reader.java new file mode 100644 index 000000000..40cb4de67 --- /dev/null +++ b/util/src/main/java/org/hyperledger/besu/util/era1/Era1Reader.java @@ -0,0 +1,127 @@ +/* + * Copyright contributors to Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.util.era1; + +import org.hyperledger.besu.util.snappy.SnappyFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.bouncycastle.util.Pack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xerial.snappy.SnappyFramedInputStream; + +/** Reads era1 files */ +public class Era1Reader { + private static final Logger LOG = LoggerFactory.getLogger(Era1Reader.class); + private static final int TYPE_LENGTH = 2; + private static final int LENGTH_LENGTH = 6; + private static final int STARTING_BLOCK_INDEX_LENGTH = 8; + private static final int BLOCK_INDEX_LENGTH = 8; + private static final int BLOCK_INDEX_COUNT_LENGTH = 8; + + private final SnappyFactory snappyFactory; + + /** + * Creates a new Era1Reader with the supplied SnappyFactory + * + * @param snappyFactory A factory to provide objects for snappy decompression + */ + public Era1Reader(final SnappyFactory snappyFactory) { + this.snappyFactory = snappyFactory; + } + + /** + * Reads the entire supplied InputStream, calling appropriate methods on the supplied + * Era1ReaderListener as different parts of the file are read + * + * @param inputStream The InputStream + * @param listener the Era1ReaderListener + * @throws IOException If there are any problems reading from the InputStream, or creating and + * using other streams, such as a SnappyFramedInputStream + */ + public void read(final InputStream inputStream, final Era1ReaderListener listener) + throws IOException { + int blockIndex = 0; + while (inputStream.available() > 0) { + Era1Type type = Era1Type.getForTypeCode(inputStream.readNBytes(TYPE_LENGTH)); + int length = (int) convertLittleEndianBytesToLong(inputStream.readNBytes(LENGTH_LENGTH)); + switch (type) { + case VERSION -> { + // do nothing + } + case EMPTY, ACCUMULATOR, TOTAL_DIFFICULTY -> { + // skip the bytes that were indicated to be empty + // TODO read ACCUMULATOR and TOTAL_DIFFICULTY properly? + inputStream.skipNBytes(length); + } + case COMPRESSED_EXECUTION_BLOCK_HEADER -> { + byte[] compressedExecutionBlockHeader = inputStream.readNBytes(length); + try (SnappyFramedInputStream decompressionStream = + snappyFactory.createFramedInputStream(compressedExecutionBlockHeader)) { + listener.handleExecutionBlockHeader( + new Era1ExecutionBlockHeader(decompressionStream.readAllBytes(), blockIndex)); + } + } + case COMPRESSED_EXECUTION_BLOCK_BODY -> { + byte[] compressedExecutionBlock = inputStream.readNBytes(length); + try (SnappyFramedInputStream decompressionStream = + snappyFactory.createFramedInputStream(compressedExecutionBlock)) { + listener.handleExecutionBlockBody( + new Era1ExecutionBlockBody(decompressionStream.readAllBytes(), blockIndex)); + } + } + case COMPRESSED_EXECUTION_BLOCK_RECEIPTS -> { + byte[] compressedReceipts = inputStream.readNBytes(length); + try (SnappyFramedInputStream decompressionStream = + snappyFactory.createFramedInputStream(compressedReceipts)) { + listener.handleExecutionBlockReceipts( + new Era1ExecutionBlockReceipts(decompressionStream.readAllBytes(), blockIndex++)); + } + } + case BLOCK_INDEX -> { + ByteArrayInputStream blockIndexInputStream = + new ByteArrayInputStream(inputStream.readNBytes(length)); + long startingBlockIndex = + convertLittleEndianBytesToLong( + blockIndexInputStream.readNBytes(STARTING_BLOCK_INDEX_LENGTH)); + List indexes = new ArrayList<>(); + while (blockIndexInputStream.available() > BLOCK_INDEX_COUNT_LENGTH) { + indexes.add( + convertLittleEndianBytesToLong( + blockIndexInputStream.readNBytes(BLOCK_INDEX_LENGTH))); + } + long indexCount = + convertLittleEndianBytesToLong( + blockIndexInputStream.readNBytes(BLOCK_INDEX_COUNT_LENGTH)); + if (indexCount != indexes.size()) { + LOG.warn( + "index count does not match number of indexes present for InputStream: {}", + inputStream); + } + listener.handleBlockIndex(new Era1BlockIndex(startingBlockIndex, indexes)); + } + } + } + } + + private long convertLittleEndianBytesToLong(final byte[] bytes) { + return Pack.littleEndianToLong(bytes, 0, bytes.length); + } +} diff --git a/util/src/main/java/org/hyperledger/besu/util/era1/Era1ReaderListener.java b/util/src/main/java/org/hyperledger/besu/util/era1/Era1ReaderListener.java new file mode 100644 index 000000000..59c35cdfb --- /dev/null +++ b/util/src/main/java/org/hyperledger/besu/util/era1/Era1ReaderListener.java @@ -0,0 +1,46 @@ +/* + * Copyright contributors to Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.util.era1; + +/** A Listener interface for listening to an Era1Reader */ +public interface Era1ReaderListener { + /** + * Handles the supplied Era1ExecutionBlockHeader + * + * @param executionBlockHeader the Era1ExecutionBlockHeader + */ + void handleExecutionBlockHeader(Era1ExecutionBlockHeader executionBlockHeader); + + /** + * Handles the supplied Era1ExecutionBlockBody + * + * @param executionBlockBody the Era1ExecutionBlockBody + */ + void handleExecutionBlockBody(Era1ExecutionBlockBody executionBlockBody); + + /** + * Handles the supplied Era1ExecutionBlockReceipts + * + * @param executionBlockReceipts the Era1ExecutionBlockReceipts + */ + void handleExecutionBlockReceipts(Era1ExecutionBlockReceipts executionBlockReceipts); + + /** + * Handles the supplied Era1BlockIndex + * + * @param blockIndex the Era1BlockIndex + */ + void handleBlockIndex(Era1BlockIndex blockIndex); +} diff --git a/util/src/main/java/org/hyperledger/besu/util/era1/Era1Type.java b/util/src/main/java/org/hyperledger/besu/util/era1/Era1Type.java new file mode 100644 index 000000000..8a8edef32 --- /dev/null +++ b/util/src/main/java/org/hyperledger/besu/util/era1/Era1Type.java @@ -0,0 +1,77 @@ +/* + * Copyright contributors to Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.util.era1; + +import java.util.HexFormat; + +/** An enumeration of known sections of era1 files */ +public enum Era1Type { + /** An empty section */ + EMPTY(new byte[] {0x00, 0x00}), + /** A snappy compressed execution block header */ + COMPRESSED_EXECUTION_BLOCK_HEADER(new byte[] {0x03, 0x00}), + /** A snappy compressed execution block body */ + COMPRESSED_EXECUTION_BLOCK_BODY(new byte[] {0x04, 0x00}), + /** A snappy compressed list of execution block transaction receipts */ + COMPRESSED_EXECUTION_BLOCK_RECEIPTS(new byte[] {0x05, 0x00}), + /** The total difficulty */ + TOTAL_DIFFICULTY(new byte[] {0x06, 0x00}), + /** The accumulator */ + ACCUMULATOR(new byte[] {0x07, 0x00}), + /** A version section */ + VERSION(new byte[] {0x65, 0x32}), + /** An execution block index */ + BLOCK_INDEX(new byte[] {0x66, 0x32}), + ; + private final byte[] typeCode; + + Era1Type(final byte[] typeCode) { + this.typeCode = typeCode; + } + + /** + * Gets the type code + * + * @return the type code + */ + public byte[] getTypeCode() { + return typeCode; + } + + /** + * Gets the Era1Type corresponding to the supplied typeCode + * + * @param typeCode the typeCode to find the corresponding Era1Type for + * @return the Era1Type corresponding to the supplied typeCode + * @throws IllegalArgumentException if there is no Era1Type corresponding to the supplied typeCode + */ + public static Era1Type getForTypeCode(final byte[] typeCode) { + if (typeCode == null || typeCode.length != 2) { + throw new IllegalArgumentException("typeCode must be 2 bytes"); + } + + Era1Type result = null; + for (Era1Type era1Type : values()) { + if (era1Type.typeCode[0] == typeCode[0] && era1Type.typeCode[1] == typeCode[1]) { + result = era1Type; + } + } + if (result == null) { + throw new IllegalArgumentException( + "typeCode " + HexFormat.of().formatHex(typeCode) + " is not recognised"); + } + return result; + } +} diff --git a/util/src/main/java/org/hyperledger/besu/util/snappy/SnappyFactory.java b/util/src/main/java/org/hyperledger/besu/util/snappy/SnappyFactory.java new file mode 100644 index 000000000..f4686ab53 --- /dev/null +++ b/util/src/main/java/org/hyperledger/besu/util/snappy/SnappyFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright contributors to Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.util.snappy; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import org.xerial.snappy.SnappyFramedInputStream; + +/** A Factory for producing objects related to handling snappy compressed data */ +public class SnappyFactory { + + /** Creates a SnappyFactory */ + public SnappyFactory() {} + + /** + * Creates a SnappyFramedInputStream reading from the supplied compressedData + * + * @param compressedData The data for the SnappyFramedInputStream to read + * @return a SnappyFramedInputStream reading from the supplied compressedData + * @throws IOException if the SnappyFramedInputStream is unable to be created + */ + public SnappyFramedInputStream createFramedInputStream(final byte[] compressedData) + throws IOException { + return new SnappyFramedInputStream(new ByteArrayInputStream(compressedData)); + } +} diff --git a/util/src/test/java/org/hyperledger/besu/util/era1/Era1ReaderTest.java b/util/src/test/java/org/hyperledger/besu/util/era1/Era1ReaderTest.java new file mode 100644 index 000000000..0f5943b57 --- /dev/null +++ b/util/src/test/java/org/hyperledger/besu/util/era1/Era1ReaderTest.java @@ -0,0 +1,229 @@ +/* + * Copyright contributors to Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.util.era1; + +import org.hyperledger.besu.util.snappy.SnappyFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.xerial.snappy.SnappyFramedInputStream; + +@ExtendWith(MockitoExtension.class) +public class Era1ReaderTest { + + private @Mock SnappyFactory snappyFactory; + + private Era1Reader reader; + + @BeforeEach + public void beforeTest() { + reader = new Era1Reader(snappyFactory); + } + + @Test + public void testReadForVersionType() throws IOException { + InputStream inputStream = Mockito.mock(InputStream.class); + Era1ReaderListener listener = Mockito.mock(Era1ReaderListener.class); + + Mockito.when(inputStream.available()).thenReturn(8, 0); + Mockito.when(inputStream.readNBytes(2)).thenReturn(Era1Type.VERSION.getTypeCode()); + Mockito.when(inputStream.readNBytes(6)) + .thenReturn(new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + + reader.read(inputStream, listener); + + Mockito.verifyNoInteractions(snappyFactory); + Mockito.verify(inputStream, Mockito.times(2)).available(); + Mockito.verify(inputStream).readNBytes(2); + Mockito.verify(inputStream).readNBytes(6); + Mockito.verifyNoInteractions(listener); + } + + @Test + public void testReadForEmptyType() throws IOException { + InputStream inputStream = Mockito.mock(InputStream.class); + Era1ReaderListener listener = Mockito.mock(Era1ReaderListener.class); + + Mockito.when(inputStream.available()).thenReturn(16, 0); + Mockito.when(inputStream.readNBytes(2)).thenReturn(Era1Type.EMPTY.getTypeCode()); + Mockito.when(inputStream.readNBytes(6)) + .thenReturn(new byte[] {0x08, 0x00, 0x00, 0x00, 0x00, 0x00}); + + reader.read(inputStream, listener); + + Mockito.verifyNoInteractions(snappyFactory); + Mockito.verify(inputStream, Mockito.times(2)).available(); + Mockito.verify(inputStream).readNBytes(2); + Mockito.verify(inputStream).readNBytes(6); + Mockito.verify(inputStream).skipNBytes(8); + Mockito.verifyNoInteractions(listener); + } + + @Test + public void testReadForCompressedExecutionBlockHeader() throws IOException { + InputStream inputStream = Mockito.mock(InputStream.class); + Era1ReaderListener listener = Mockito.mock(Era1ReaderListener.class); + SnappyFramedInputStream snappyFramedInputStream = Mockito.mock(SnappyFramedInputStream.class); + + Mockito.when(inputStream.available()).thenReturn(15, 0); + Mockito.when(inputStream.readNBytes(2)) + .thenReturn(Era1Type.COMPRESSED_EXECUTION_BLOCK_HEADER.getTypeCode()); + Mockito.when(inputStream.readNBytes(6)) + .thenReturn(new byte[] {0x07, 0x00, 0x00, 0x00, 0x00, 0x00}); + byte[] compressedExecutionBlockHeader = new byte[] {1, 2, 3, 4, 5, 6, 7}; + Mockito.when(inputStream.readNBytes(7)).thenReturn(compressedExecutionBlockHeader); + Mockito.when(snappyFactory.createFramedInputStream(compressedExecutionBlockHeader)) + .thenReturn(snappyFramedInputStream); + byte[] executionBlockHeader = new byte[] {10, 9, 8, 7, 6, 5, 4, 3, 2, 1}; + Mockito.when(snappyFramedInputStream.readAllBytes()).thenReturn(executionBlockHeader); + + reader.read(inputStream, listener); + + Mockito.verify(inputStream, Mockito.times(2)).available(); + Mockito.verify(inputStream).readNBytes(2); + Mockito.verify(inputStream).readNBytes(6); + Mockito.verify(inputStream).readNBytes(7); + Mockito.verify(snappyFactory).createFramedInputStream(compressedExecutionBlockHeader); + Mockito.verify(snappyFramedInputStream).readAllBytes(); + ArgumentCaptor executionBlockHeaderArgumentCaptor = + ArgumentCaptor.forClass(Era1ExecutionBlockHeader.class); + Mockito.verify(listener) + .handleExecutionBlockHeader(executionBlockHeaderArgumentCaptor.capture()); + Mockito.verifyNoMoreInteractions(listener); + + Era1ExecutionBlockHeader era1ExecutionBlockHeader = + executionBlockHeaderArgumentCaptor.getValue(); + Assertions.assertEquals(executionBlockHeader, era1ExecutionBlockHeader.header()); + Assertions.assertEquals(0, era1ExecutionBlockHeader.blockIndex()); + } + + @Test + public void testReadForCompressedExecutionBlockBody() throws IOException { + InputStream inputStream = Mockito.mock(InputStream.class); + Era1ReaderListener listener = Mockito.mock(Era1ReaderListener.class); + SnappyFramedInputStream snappyFramedInputStream = Mockito.mock(SnappyFramedInputStream.class); + + Mockito.when(inputStream.available()).thenReturn(15, 0); + Mockito.when(inputStream.readNBytes(2)) + .thenReturn(Era1Type.COMPRESSED_EXECUTION_BLOCK_BODY.getTypeCode()); + Mockito.when(inputStream.readNBytes(6)) + .thenReturn(new byte[] {0x07, 0x00, 0x00, 0x00, 0x00, 0x00}); + byte[] compressedExecutionBlockBody = new byte[] {1, 2, 3, 4, 5, 6, 7}; + Mockito.when(inputStream.readNBytes(7)).thenReturn(compressedExecutionBlockBody); + Mockito.when(snappyFactory.createFramedInputStream(compressedExecutionBlockBody)) + .thenReturn(snappyFramedInputStream); + byte[] executionBlockBody = new byte[] {10, 9, 8, 7, 6, 5, 4, 3, 2, 1}; + Mockito.when(snappyFramedInputStream.readAllBytes()).thenReturn(executionBlockBody); + + reader.read(inputStream, listener); + + Mockito.verify(inputStream, Mockito.times(2)).available(); + Mockito.verify(inputStream).readNBytes(2); + Mockito.verify(inputStream).readNBytes(6); + Mockito.verify(inputStream).readNBytes(7); + Mockito.verify(snappyFactory).createFramedInputStream(compressedExecutionBlockBody); + Mockito.verify(snappyFramedInputStream).readAllBytes(); + ArgumentCaptor executionBlockBodyArgumentCaptor = + ArgumentCaptor.forClass(Era1ExecutionBlockBody.class); + Mockito.verify(listener).handleExecutionBlockBody(executionBlockBodyArgumentCaptor.capture()); + Mockito.verifyNoMoreInteractions(listener); + + Era1ExecutionBlockBody era1ExecutionBlockBody = executionBlockBodyArgumentCaptor.getValue(); + Assertions.assertEquals(executionBlockBody, era1ExecutionBlockBody.block()); + Assertions.assertEquals(0, era1ExecutionBlockBody.blockIndex()); + } + + @Test + public void testReadForCompressedExecutionBlockReceipts() throws IOException { + InputStream inputStream = Mockito.mock(InputStream.class); + Era1ReaderListener listener = Mockito.mock(Era1ReaderListener.class); + SnappyFramedInputStream snappyFramedInputStream = Mockito.mock(SnappyFramedInputStream.class); + + Mockito.when(inputStream.available()).thenReturn(15, 0); + Mockito.when(inputStream.readNBytes(2)) + .thenReturn(Era1Type.COMPRESSED_EXECUTION_BLOCK_RECEIPTS.getTypeCode()); + Mockito.when(inputStream.readNBytes(6)) + .thenReturn(new byte[] {0x07, 0x00, 0x00, 0x00, 0x00, 0x00}); + byte[] compressedExecutionBlockReceipts = new byte[] {1, 2, 3, 4, 5, 6, 7}; + Mockito.when(inputStream.readNBytes(7)).thenReturn(compressedExecutionBlockReceipts); + Mockito.when(snappyFactory.createFramedInputStream(compressedExecutionBlockReceipts)) + .thenReturn(snappyFramedInputStream); + byte[] executionBlockReceipts = new byte[] {10, 9, 8, 7, 6, 5, 4, 3, 2, 1}; + Mockito.when(snappyFramedInputStream.readAllBytes()).thenReturn(executionBlockReceipts); + + reader.read(inputStream, listener); + + Mockito.verify(inputStream, Mockito.times(2)).available(); + Mockito.verify(inputStream).readNBytes(2); + Mockito.verify(inputStream).readNBytes(6); + Mockito.verify(inputStream).readNBytes(7); + Mockito.verify(snappyFactory).createFramedInputStream(compressedExecutionBlockReceipts); + Mockito.verify(snappyFramedInputStream).readAllBytes(); + ArgumentCaptor executionBlockReceiptsArgumentCaptor = + ArgumentCaptor.forClass(Era1ExecutionBlockReceipts.class); + Mockito.verify(listener) + .handleExecutionBlockReceipts(executionBlockReceiptsArgumentCaptor.capture()); + Mockito.verifyNoMoreInteractions(listener); + + Era1ExecutionBlockReceipts era1ExecutionBlockReceipts = + executionBlockReceiptsArgumentCaptor.getValue(); + Assertions.assertEquals(executionBlockReceipts, era1ExecutionBlockReceipts.receipts()); + Assertions.assertEquals(0, era1ExecutionBlockReceipts.blockIndex()); + } + + @Test + public void testReadForBlockIndexType() throws IOException { + InputStream inputStream = Mockito.mock(InputStream.class); + Era1ReaderListener listener = Mockito.mock(Era1ReaderListener.class); + + Mockito.when(inputStream.available()).thenReturn(40, 0); + Mockito.when(inputStream.readNBytes(2)).thenReturn(Era1Type.BLOCK_INDEX.getTypeCode()); + Mockito.when(inputStream.readNBytes(6)) + .thenReturn(new byte[] {0x20, 0x00, 0x00, 0x00, 0x00, 0x00}); + Mockito.when(inputStream.readNBytes(32)) + .thenReturn( + new byte[] { + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + }); + + reader.read(inputStream, listener); + + Mockito.verifyNoInteractions(snappyFactory); + Mockito.verify(inputStream, Mockito.times(2)).available(); + Mockito.verify(inputStream).readNBytes(2); + Mockito.verify(inputStream).readNBytes(6); + Mockito.verify(inputStream).readNBytes(32); + ArgumentCaptor blockIndexArgumentCaptor = + ArgumentCaptor.forClass(Era1BlockIndex.class); + Mockito.verify(listener).handleBlockIndex(blockIndexArgumentCaptor.capture()); + Mockito.verifyNoMoreInteractions(listener); + + Era1BlockIndex blockIndex = blockIndexArgumentCaptor.getValue(); + Assertions.assertEquals(1, blockIndex.startingBlockIndex()); + Assertions.assertEquals(List.of(2L, 3L), blockIndex.indexes()); + } +}