diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/util/BlockchainUtil.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/util/BlockchainUtil.java index e8bdfd750a..e349bdae05 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/util/BlockchainUtil.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/util/BlockchainUtil.java @@ -12,6 +12,17 @@ public class BlockchainUtil { private BlockchainUtil() {} + /** + * General utility to process a list of headers and a blockchain, sussing out which header in the + * input list is simultaneously the highest order block number and a direct match with one of the + * headers of the local chain. The purpose of which being to determine the point of departure in + * fork scenarios. + * + * @param blockchain our local copy of the blockchain + * @param headers the list of remote headers + * @param ascendingHeaderOrder whether the headers are sorted in ascending or descending order + * @return index of the highest known header, or an empty value if no header is known + */ public static OptionalInt findHighestKnownBlockIndex( final Blockchain blockchain, final List headers, diff --git a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/util/BlockchainUtilParameterizedTest.java b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/util/BlockchainUtilParameterizedTest.java new file mode 100644 index 0000000000..6fe3b5730d --- /dev/null +++ b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/util/BlockchainUtilParameterizedTest.java @@ -0,0 +1,135 @@ +package tech.pegasys.pantheon.ethereum.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.BlockBody; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.TransactionReceipt; +import tech.pegasys.pantheon.ethereum.db.DefaultMutableBlockchain; +import tech.pegasys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import tech.pegasys.pantheon.ethereum.testutil.BlockDataGenerator; +import tech.pegasys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import tech.pegasys.pantheon.services.kvstore.KeyValueStorage; +import tech.pegasys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.OptionalInt; +import java.util.Random; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class BlockchainUtilParameterizedTest { + private static final BlockDataGenerator blockDataGenerator = new BlockDataGenerator(); + private static final Random random = new Random(1337); + + private static final int chainHeight = 89; + private final int commonAncestorHeight; + private static Block genesisBlock; + private static KeyValueStorage localKvStore; + private static DefaultMutableBlockchain localBlockchain; + + private KeyValueStorage remoteKvStore; + private DefaultMutableBlockchain remoteBlockchain; + + private BlockHeader commonHeader; + private List headers; + + public BlockchainUtilParameterizedTest(final int commonAncestorHeight) { + this.commonAncestorHeight = commonAncestorHeight; + } + + @BeforeClass + public static void setupClass() { + genesisBlock = blockDataGenerator.genesisBlock(); + localKvStore = new InMemoryKeyValueStorage(); + localBlockchain = + new DefaultMutableBlockchain( + genesisBlock, localKvStore, MainnetBlockHashFunction::createHash); + // Setup local chain. + for (int i = 1; i <= chainHeight; i++) { + final BlockDataGenerator.BlockOptions options = + new BlockDataGenerator.BlockOptions() + .setBlockNumber(i) + .setParentHash(localBlockchain.getBlockHashByNumber(i - 1).get()); + final Block block = blockDataGenerator.block(options); + final List receipts = blockDataGenerator.receipts(block); + localBlockchain.appendBlock(block, receipts); + } + } + + @Before + public void setup() { + remoteKvStore = new InMemoryKeyValueStorage(); + remoteBlockchain = + new DefaultMutableBlockchain( + genesisBlock, remoteKvStore, MainnetBlockHashFunction::createHash); + + commonHeader = genesisBlock.getHeader(); + for (long i = 1; i <= commonAncestorHeight; i++) { + commonHeader = localBlockchain.getBlockHeader(i).get(); + final List receipts = + localBlockchain.getTxReceipts(commonHeader.getHash()).get(); + final BlockBody commonBody = localBlockchain.getBlockBody(commonHeader.getHash()).get(); + remoteBlockchain.appendBlock(new Block(commonHeader, commonBody), receipts); + } + // Remaining blocks are disparate. + for (long i = commonAncestorHeight + 1L; i <= chainHeight; i++) { + final BlockDataGenerator.BlockOptions localOptions = + new BlockDataGenerator.BlockOptions() + .setBlockNumber(i) + .setParentHash(localBlockchain.getBlockHashByNumber(i - 1).get()); + final Block localBlock = blockDataGenerator.block(localOptions); + final List localReceipts = blockDataGenerator.receipts(localBlock); + localBlockchain.appendBlock(localBlock, localReceipts); + + final BlockDataGenerator.BlockOptions remoteOptions = + new BlockDataGenerator.BlockOptions() + .setDifficulty(UInt256.ONE) // differentiator + .setBlockNumber(i) + .setParentHash(remoteBlockchain.getBlockHashByNumber(i - 1).get()); + final Block remoteBlock = blockDataGenerator.block(remoteOptions); + final List remoteReceipts = blockDataGenerator.receipts(remoteBlock); + remoteBlockchain.appendBlock(remoteBlock, remoteReceipts); + } + headers = new ArrayList<>(); + for (long i = 0L; i <= remoteBlockchain.getChainHeadBlockNumber(); i++) { + headers.add(remoteBlockchain.getBlockHeader(i).get()); + } + } + + @Parameterized.Parameters(name = "commonAncestor={0}") + public static Collection parameters() { + final List params = new ArrayList<>(); + params.add(new Object[] {0}); + params.add(new Object[] {chainHeight}); + params.add(new Object[] {random.nextInt(chainHeight - 1) + 1}); + params.add(new Object[] {random.nextInt(chainHeight - 1) + 1}); + params.add(new Object[] {random.nextInt(chainHeight - 1) + 1}); + return params; + } + + @Test + public void searchesAscending() { + OptionalInt maybeAncestorNumber = + BlockchainUtil.findHighestKnownBlockIndex(localBlockchain, headers, true); + assertThat(maybeAncestorNumber.getAsInt()).isEqualTo(Math.toIntExact(commonHeader.getNumber())); + } + + @Test + public void searchesDescending() { + Collections.reverse(headers); + OptionalInt maybeAncestorNumber = + BlockchainUtil.findHighestKnownBlockIndex(localBlockchain, headers, false); + assertThat(maybeAncestorNumber.getAsInt()) + .isEqualTo(Math.toIntExact(chainHeight - commonHeader.getNumber())); + } +} diff --git a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/util/BlockchainUtilTest.java b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/util/BlockchainUtilTest.java deleted file mode 100644 index 69c41adbc3..0000000000 --- a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/util/BlockchainUtilTest.java +++ /dev/null @@ -1,119 +0,0 @@ -package tech.pegasys.pantheon.ethereum.util; - -import static org.assertj.core.api.Assertions.assertThat; -import static sun.security.krb5.Confounder.bytes; - -import tech.pegasys.pantheon.ethereum.core.Address; -import tech.pegasys.pantheon.ethereum.core.Block; -import tech.pegasys.pantheon.ethereum.core.BlockBody; -import tech.pegasys.pantheon.ethereum.core.BlockHeader; -import tech.pegasys.pantheon.ethereum.core.BlockHeaderBuilder; -import tech.pegasys.pantheon.ethereum.core.Hash; -import tech.pegasys.pantheon.ethereum.core.LogsBloomFilter; -import tech.pegasys.pantheon.ethereum.core.TransactionReceipt; -import tech.pegasys.pantheon.ethereum.db.DefaultMutableBlockchain; -import tech.pegasys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; -import tech.pegasys.pantheon.ethereum.testutil.BlockDataGenerator; -import tech.pegasys.pantheon.services.kvstore.InMemoryKeyValueStorage; -import tech.pegasys.pantheon.services.kvstore.KeyValueStorage; -import tech.pegasys.pantheon.util.bytes.Bytes32; -import tech.pegasys.pantheon.util.bytes.BytesValue; -import tech.pegasys.pantheon.util.uint.UInt256; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.OptionalInt; - -import org.junit.Test; - -public class BlockchainUtilTest { - - @Test - public void shouldReturnIndexOfCommonBlockForAscendingOrder() { - BlockDataGenerator blockDataGenerator = new BlockDataGenerator(); - - BlockHeader genesisHeader = - BlockHeaderBuilder.create() - .parentHash(Hash.ZERO) - .ommersHash(Hash.ZERO) - .coinbase(Address.fromHexString("0x0000000000000000000000000000000000000000")) - .stateRoot(Hash.ZERO) - .transactionsRoot(Hash.ZERO) - .receiptsRoot(Hash.ZERO) - .logsBloom(new LogsBloomFilter(BytesValue.of(bytes(LogsBloomFilter.BYTE_SIZE)))) - .difficulty(UInt256.ZERO) - .number(0L) - .gasLimit(1L) - .gasUsed(1L) - .timestamp(Instant.now().truncatedTo(ChronoUnit.SECONDS).getEpochSecond()) - .extraData(Bytes32.wrap(bytes(Bytes32.SIZE))) - .mixHash(Hash.ZERO) - .nonce(0L) - .blockHashFunction(MainnetBlockHashFunction::createHash) - .buildBlockHeader(); - BlockBody genesisBody = new BlockBody(Collections.emptyList(), Collections.emptyList()); - Block genesisBlock = new Block(genesisHeader, genesisBody); - - KeyValueStorage kvStoreLocal = new InMemoryKeyValueStorage(); - KeyValueStorage kvStoreRemote = new InMemoryKeyValueStorage(); - - DefaultMutableBlockchain blockchainLocal = - new DefaultMutableBlockchain( - genesisBlock, kvStoreLocal, MainnetBlockHashFunction::createHash); - DefaultMutableBlockchain blockchainRemote = - new DefaultMutableBlockchain( - genesisBlock, kvStoreRemote, MainnetBlockHashFunction::createHash); - - // Common chain segment - Block commonBlock = null; - for (long i = 1; i <= 3; i++) { - BlockDataGenerator.BlockOptions options = - new BlockDataGenerator.BlockOptions() - .setBlockNumber(i) - .setParentHash(blockchainLocal.getBlockHashByNumber(i - 1).get()); - commonBlock = blockDataGenerator.block(options); - List receipts = blockDataGenerator.receipts(commonBlock); - blockchainLocal.appendBlock(commonBlock, receipts); - blockchainRemote.appendBlock(commonBlock, receipts); - } - - // Populate local chain - for (long i = 4; i <= 9; i++) { - BlockDataGenerator.BlockOptions optionsLocal = - new BlockDataGenerator.BlockOptions() - .setDifficulty(UInt256.ZERO) // differentiator - .setBlockNumber(i) - .setParentHash(blockchainLocal.getBlockHashByNumber(i - 1).get()); - Block blockLocal = blockDataGenerator.block(optionsLocal); - List receiptsLocal = blockDataGenerator.receipts(blockLocal); - blockchainLocal.appendBlock(blockLocal, receiptsLocal); - } - - // Populate remote chain - for (long i = 4; i <= 9; i++) { - BlockDataGenerator.BlockOptions optionsRemote = - new BlockDataGenerator.BlockOptions() - .setDifficulty(UInt256.ONE) - .setBlockNumber(i) - .setParentHash(blockchainRemote.getBlockHashByNumber(i - 1).get()); - Block blockRemote = blockDataGenerator.block(optionsRemote); - List receiptsRemote = blockDataGenerator.receipts(blockRemote); - blockchainRemote.appendBlock(blockRemote, receiptsRemote); - } - - // Create a list of headers... - List headers = new ArrayList<>(); - for (long i = 0L; i < blockchainRemote.getChainHeadBlockNumber(); i++) { - headers.add(blockchainRemote.getBlockHeader(i).get()); - } - - OptionalInt maybeAncestorNumber = - BlockchainUtil.findHighestKnownBlockIndex(blockchainLocal, headers, true); - - assertThat(maybeAncestorNumber.getAsInt()) - .isEqualTo(Math.toIntExact(commonBlock.getHeader().getNumber())); - } -}