content: package org.aion.zero.impl.blockchain;
  
  import static com.google.common.truth.Truth.assertThat;
  import static org.aion.zero.impl.blockchain.BlockchainTestUtils.generateRandomChain;
  import static org.aion.zero.impl.blockchain.BlockchainTestUtils.generateRandomChainWithoutTransactions;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import java.util.Optional;
  import org.aion.base.AionTransaction;
  import org.aion.crypto.ECKey;
  import org.aion.db.impl.mockdb.MockDB;
  import org.aion.log.AionLoggerFactory;
  import org.aion.zero.impl.types.Block;
  import org.aion.zero.impl.types.BlockHeader;
  import org.aion.zero.impl.config.CfgPrune;
  import org.aion.zero.impl.core.FastImportResult;
  import org.aion.zero.impl.core.ImportResult;
  import org.aion.util.types.ByteArrayWrapper;
  import org.aion.zero.impl.types.BlockContext;
  import org.aion.zero.impl.db.MockRepositoryConfig;
  import org.aion.zero.impl.db.AionRepositoryImpl;
  import org.aion.zero.impl.types.MiningBlock;
  import org.aion.zero.impl.types.MiningBlockHeader;
  import org.aion.zero.impl.vm.AvmTestConfig;
  import org.apache.commons.lang3.tuple.Pair;
  import org.junit.AfterClass;
  import org.junit.BeforeClass;
  import org.junit.Test;
  
  /**
   * Class for testing methods from {@link AionBlockchainImpl} that are not part of specific use
   * cases.
   *
   * @author Alexandra Roatis
   */
  public class BlockchainImplementationTest {
  
      private static final List<ECKey> accounts = BlockchainTestUtils.generateAccounts(10);
      private static final int MAX_TX_PER_BLOCK = 30;
  
      @BeforeClass
      public static void beforeClass() {
          AionLoggerFactory.initAll();
  
          AvmTestConfig.supportOnlyAvmVersion1();
      }
  
      @AfterClass
      public static void tearDown() {
          AvmTestConfig.clearConfigurations();
      }
  
      /**
       * In TOP mode only the top K blocks have a stored state. Blocks older than the top K are have
       * restrictions due to pruning.
       */
      @Test
      public void testIsPruneRestricted_wTopState() {
          // number of blocks stored by the blockchain
          int stored = 130; // note that min is 128
          // the maximum height considered by this test
          int height = 200;
  
          StandaloneBlockchain.Bundle bundle =
                  new StandaloneBlockchain.Builder()
                          .withValidatorConfiguration("simple")
                          .withRepoConfig(new MockRepositoryConfig(new CfgPrune(stored)))
                          .withDefaultAccounts(accounts)
                          .build();
  
          StandaloneBlockchain chain = bundle.bc;
          AionRepositoryImpl repo = chain.getRepository();
          MiningBlock importBlock, sidechainBlock;
          List<AionTransaction> txs;
  
          // creating (height) blocks
          long time = System.currentTimeMillis();
          for (int i = 0; i < stored; i++) {
              txs = BlockchainTestUtils.generateTransactions(MAX_TX_PER_BLOCK, accounts, repo);
              importBlock = chain.createNewMiningBlockInternal(chain.getBestBlock(), txs, true, time / 10000L).block;
              assertThat(chain.tryToConnect(importBlock)).isEqualTo(ImportResult.IMPORTED_BEST);
          }
  
          Block fork = chain.getBestBlock();
  
          for (int i = stored; i < height; i++) {
              txs = BlockchainTestUtils.generateTransactions(MAX_TX_PER_BLOCK, accounts, repo);
              importBlock = chain.createNewMiningBlockInternal(chain.getBestBlock(), txs, true, time / 10000L).block;
              assertThat(chain.tryToConnect(importBlock)).isEqualTo(ImportResult.IMPORTED_BEST);
  
              // create the sidechain block
              repo.syncToRoot(fork.getStateRoot());
              txs = BlockchainTestUtils.generateTransactions(MAX_TX_PER_BLOCK, accounts, repo);
              repo.syncToRoot(chain.getBestBlock().getStateRoot());
              sidechainBlock =chain.createNewMiningBlockInternal(fork, txs, true, (time - 10) / 10000L).block;
              assertThat(chain.tryToConnect(sidechainBlock)).isEqualTo(ImportResult.IMPORTED_NOT_BEST);
              fork = sidechainBlock;
          }
  
          // testing restriction for unrestricted blocks: height to (height - stored + 1)
          for (int i = height; i >= height - stored + 1; i--) {
              assertThat(chain.isPruneRestricted(i)).isFalse();
              // ensure the state exists
              assertThat(repo.isValidRoot(chain.getBlockByNumber(i).getStateRoot())).isTrue();
          }
  
          // testing restriction for restricted blocks: (height - stored) to 0
          for (int i = height - stored; i >= 0; i--) {
              assertThat(chain.isPruneRestricted(i)).isTrue();
              // ensure the state is missing
              if (i < height - stored) {
                  assertThat(repo.isValidRoot(chain.getBlockByNumber(i).getStateRoot())).isFalse();
              } else {
                  // NOTE: state at (height - stored) exists, but is already restricted
                  assertThat(repo.isValidRoot(chain.getBlockByNumber(i).getStateRoot())).isTrue();
              }
          }
      }
  
      /**
       * In FULL mode the state is stored for all blocks. There are no restrictions due to pruning.
       */
      @Test
      public void testIsPruneRestricted_wFullState() {
          // the maximum height considered by this test
          int height = 200;
  
          StandaloneBlockchain.Bundle bundle =
                  new StandaloneBlockchain.Builder()
                          .withValidatorConfiguration("simple")
                          .withRepoConfig(new MockRepositoryConfig(new CfgPrune(false)))
                          .withDefaultAccounts(accounts)
                          .build();
  
          StandaloneBlockchain chain = bundle.bc;
          AionRepositoryImpl repo = chain.getRepository();
          BlockContext context;
          List<AionTransaction> txs;
  
          // creating (height) blocks
          long time = System.currentTimeMillis();
          for (int i = 0; i < height; i++) {
              txs = BlockchainTestUtils.generateTransactions(MAX_TX_PER_BLOCK, accounts, repo);
              context = chain.createNewMiningBlockInternal(chain.getBestBlock(), txs, true, time / 10000L);
              assertThat(chain.tryToConnect(context.block)).isEqualTo(ImportResult.IMPORTED_BEST);
          }
  
          // testing restriction for unrestricted blocks
          for (int i = height; i >= 0; i--) {
              assertThat(chain.isPruneRestricted(i)).isFalse();
              // ensure the state exists
              assertThat(repo.isValidRoot(chain.getBlockByNumber(i).getStateRoot())).isTrue();
          }
      }
  
      /**
       * In SPREAD mode the top K blocks and the blocks that are multiples of the archive rate have a
       * stored state. There are no restrictions due to pruning.
       */
      @Test
      public void testIsPruneRestricted_wSpreadState() {
          // number of blocks stored by the blockchain
          int stored = 150;
          // the maximum height considered by this test
          int height = 1200;
          // the interval at which blocks are indexed
          int index = 1000;
  
          StandaloneBlockchain.Bundle bundle =
                  new StandaloneBlockchain.Builder()
                          .withValidatorConfiguration("simple")
                          .withRepoConfig(new MockRepositoryConfig(new CfgPrune(stored, index)))
                          .withDefaultAccounts(accounts)
                          .build();
  
          StandaloneBlockchain chain = bundle.bc;
          AionRepositoryImpl repo = chain.getRepository();
          BlockContext context;
          List<AionTransaction> txs;
  
          // creating (height) blocks
          long time = System.currentTimeMillis();
          for (int i = 0; i < height; i++) {
              txs = BlockchainTestUtils.generateTransactions(MAX_TX_PER_BLOCK, accounts, repo);
              context = chain.createNewMiningBlockInternal(chain.getBestBlock(), txs, true, time / 100000L);
              assertThat(chain.tryToConnect(context.block)).isEqualTo(ImportResult.IMPORTED_BEST);
          }
  
          // testing restriction for unrestricted blocks
          for (int i = height; i >= 0; i--) {
              assertThat(chain.isPruneRestricted(i)).isFalse();
              if (i % index == 0 || i >= height - stored) {
                  // ensure the state exists
                  assertThat(repo.isValidRoot(chain.getBlockByNumber(i).getStateRoot())).isTrue();
              } else {
                  // ensure the state is missing
                  assertThat(repo.isValidRoot(chain.getBlockByNumber(i).getStateRoot())).isFalse();
              }
          }
      }
  
      @Test(expected = NullPointerException.class)
      public void testGetTotalDifficultyByHash_wNull() {
          StandaloneBlockchain.Bundle bundle =
                  new StandaloneBlockchain.Builder().withValidatorConfiguration("simple").build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          chain.getTotalDifficultyForHash(null);
      }
  
      @Test
      public void testSkipTryToConnect() {
          // the maximum height considered by this test
          int height = 200;
  
          StandaloneBlockchain.Bundle bundle =
                  new StandaloneBlockchain.Builder()
                          .withValidatorConfiguration("simple")
                          .withDefaultAccounts(accounts)
                          .build();
  
          StandaloneBlockchain chain = bundle.bc;
          AionRepositoryImpl repo = chain.getRepository();
          MiningBlock block;
          List<AionTransaction> txs;
  
          // creating (height) blocks
          long time = System.currentTimeMillis();
          for (int i = 0; i < height; i++) {
              txs = BlockchainTestUtils.generateTransactions(MAX_TX_PER_BLOCK, accounts, repo);
              block =
                      chain.createNewMiningBlockInternal(chain.getBestBlock(), txs, true, time / 10000L)
                              .block;
              assertThat(chain.tryToConnect(block)).isEqualTo(ImportResult.IMPORTED_BEST);
  
              for (int j = 0; j < block.getNumber() + 100; j++) {
                  if (j < block.getNumber() - 32 || j > block.getNumber() + 32) {
                      // check outside import range
                      assertThat(chain.skipTryToConnect(j)).isTrue();
                  } else {
                      // check inside import range
                      assertThat(chain.skipTryToConnect(j)).isFalse();
                  }
              }
          }
      }
  
      @Test(expected = NullPointerException.class)
      public void testGetListOfHashesEndWith_wNullHash() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle =
                  builder.withValidatorConfiguration("simple").withDefaultAccounts(accounts).build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChain(chain, 6, 2, accounts, MAX_TX_PER_BLOCK);
  
          chain.getListOfHashesEndWith(null, 1);
      }
  
      @Test
      public void testGetListOfHeadersEndWith_wGenesisBlock() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle =
                  builder.withValidatorConfiguration("simple").withDefaultAccounts(accounts).build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChain(chain, 6, 2, accounts, MAX_TX_PER_BLOCK);
  
          MiningBlock genesis = chain.getGenesis();
          byte[] genesisHash = genesis.getHash();
  
          int qty;
          // get one block
          qty = 1;
          assertEndWith_expectedOne(chain, genesisHash, qty);
  
          // get list of 10 from genesis
          qty = 10; // larger than block height
          assertEndWith_expectedOne(chain, genesisHash, qty);
  
          // get list of -10 from genesis
          qty = -10; // negative value
          assertEndWith_expectedOne(chain, genesisHash, qty);
  
          // get list of 0 from genesis
          qty = 0; // zero blocks requested
          assertEndWith_expectedOne(chain, genesisHash, qty);
      }
  
      @Test
      public void testGetListOfHeadersEndWith_wBestBlock() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle =
                  builder.withValidatorConfiguration("simple").withDefaultAccounts(accounts).build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChain(chain, 12, 2, accounts, MAX_TX_PER_BLOCK);
  
          Block best = chain.getBestBlock();
          assertThat(best.getNumber()).isGreaterThan(0L);
          byte[] bestHash = best.getHash();
  
          // expected result with qty >= chain height
          List<ByteArrayWrapper> expectedAll = new ArrayList<>();
          Block block = best;
          while (block.getNumber() > 0) {
              expectedAll.add(ByteArrayWrapper.wrap(block.getHash()));
              block = chain.getBlockByHash(block.getParentHash());
          }
  
          int qty;
          // get one block
          qty = 1;
          assertEndWith_expectedOne(chain, bestHash, qty);
  
          // get list of chain height from best block
          qty = (int) best.getNumber(); // chain height
          assertEndWith(chain, bestHash, qty, expectedAll);
  
          // get list of half from best block
          qty = (int) (best.getNumber() / 2);
  
          List<ByteArrayWrapper> expectedHalf = new ArrayList<>();
          block = best;
          for (int i = 0; i < qty; i++) {
              expectedHalf.add(ByteArrayWrapper.wrap(block.getHash()));
              block = chain.getBlockByHash(block.getParentHash());
          }
  
          assertEndWith(chain, bestHash, qty, expectedHalf);
  
          // get list of 20 from genesis
          qty = 24; // larger than block height
          expectedAll.add(ByteArrayWrapper.wrap(chain.getGenesis().getHash()));
          assertEndWith(chain, bestHash, qty, expectedAll);
  
          // get list of -10 from best block
          qty = -10; // negative value
          assertEndWith_expectedOne(chain, bestHash, qty);
  
          // get list of 0 from best block
          qty = 0; // zero blocks requested
          assertEndWith_expectedOne(chain, bestHash, qty);
      }
  
      public static void assertEndWith_expectedOne(StandaloneBlockchain chain, byte[] hash, int qty) {
          List<byte[]> hashes = chain.getListOfHashesEndWith(hash, qty);
          assertThat(hashes.size()).isEqualTo(1);
          assertThat(hashes.get(0)).isEqualTo(hash);
      }
  
      public static void assertEndWith(
              StandaloneBlockchain chain, byte[] hash, int qty, List<ByteArrayWrapper> expected) {
          List<ByteArrayWrapper> hashes = new ArrayList<>();
          chain.getListOfHashesEndWith(hash, qty).forEach(h -> hashes.add(ByteArrayWrapper.wrap(h)));
          assertThat(hashes.size()).isEqualTo(expected.size());
          assertThat(hashes).isEqualTo(expected);
      }
  
      @Test
      public void testGetListOfHeadersStartFromBlock_wGenesisBlock() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle =
                  builder.withValidatorConfiguration("simple").withDefaultAccounts(accounts).build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChain(chain, 12, 2, accounts, MAX_TX_PER_BLOCK);
  
          MiningBlock genesis = chain.getGenesis();
          Block best = chain.getBestBlock();
  
          assertThat(best.getNumber()).isGreaterThan(0L);
          byte[] genesisHash = genesis.getHash();
  
          // expected result with qty >= chain height
          List<ByteArrayWrapper> expectedAll = new ArrayList<>();
          Block block = chain.getBlockByHash(best.getParentHash());
          while (block.getNumber() > 0) {
              expectedAll.add(0, ByteArrayWrapper.wrap(block.getHash()));
              block = chain.getBlockByHash(block.getParentHash());
          }
          expectedAll.add(0, ByteArrayWrapper.wrap(genesis.getHash()));
  
          int qty;
          // get one block
          qty = 1;
          assertStartFromBlock_expectedOne(chain, genesisHash, 0, qty);
  
          // get list of chain height from genesis block
          qty = (int) best.getNumber(); // chain height
          assertStartFromBlock(chain, 0, qty, expectedAll);
  
          // get list of half from genesis block
          qty = (int) (best.getNumber() / 2);
  
          List<ByteArrayWrapper> expectedHalf = new ArrayList<>();
          block = chain.getBlockByNumber(qty - 1);
          for (int i = 0; i < qty; i++) {
              expectedHalf.add(0, ByteArrayWrapper.wrap(block.getHash()));
              block = chain.getBlockByHash(block.getParentHash());
          }
          assertStartFromBlock(chain, 0, qty, expectedHalf);
  
          // get list of 20 from genesis
          qty = 24; // larger than block height
          expectedAll.add(ByteArrayWrapper.wrap(best.getHash()));
          assertStartFromBlock(chain, 0, qty, expectedAll);
  
          // get list of -10 from genesis block
          qty = -10; // negative value
          assertStartFromBlock_expectedOne(chain, genesisHash, 0, qty);
  
          // get list of 0 from genesis block
          qty = 0; // zero blocks requested
          assertStartFromBlock_expectedOne(chain, genesisHash, 0, qty);
      }
  
      @Test
      public void testGetListOfHeadersStartFromBlock_wBestBlock() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle =
                  builder.withValidatorConfiguration("simple").withDefaultAccounts(accounts).build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChain(chain, 6, 2, accounts, MAX_TX_PER_BLOCK);
  
          Block best = chain.getBestBlock();
          byte[] bestHash = best.getHash();
          long bestNumber = best.getNumber();
  
          int qty;
          // get one block
          qty = 1;
          assertStartFromBlock_expectedOne(chain, bestHash, bestNumber, qty);
  
          // get list of 10 from genesis
          qty = 10; // larger than block height
          assertStartFromBlock_expectedOne(chain, bestHash, bestNumber, qty);
  
          // get list of -10 from genesis
          qty = -10; // negative value
          assertStartFromBlock_expectedOne(chain, bestHash, bestNumber, qty);
  
          // get list of 0 from genesis
          qty = 0; // zero blocks requested
          assertStartFromBlock_expectedOne(chain, bestHash, bestNumber, qty);
      }
  
      public static void assertStartFromBlock_expectedOne(
              StandaloneBlockchain chain, byte[] hash, long number, int qty) {
          List<byte[]> hashes = chain.getListOfHashesStartFromBlock(number, qty);
          assertThat(hashes.size()).isEqualTo(1);
          assertThat(hashes.get(0)).isEqualTo(hash);
      }
  
      public static void assertStartFromBlock(
              StandaloneBlockchain chain, long number, int qty, List<ByteArrayWrapper> expected) {
          List<ByteArrayWrapper> hashes = new ArrayList<>();
          chain.getListOfHashesStartFromBlock(number, qty)
                  .forEach(h -> hashes.add(ByteArrayWrapper.wrap(h)));
          assertThat(hashes.size()).isEqualTo(expected.size());
          assertThat(hashes).isEqualTo(expected);
      }
  
      @Test
      public void testGetListOfHeadersStartFrom_wNullBlock() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle =
                  builder.withValidatorConfiguration("simple").withDefaultAccounts(accounts).build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChain(chain, 6, 2, accounts, MAX_TX_PER_BLOCK);
  
          assertThat(chain.getListOfHeadersStartFrom(chain.getBestBlock().getNumber() + 1, 1))
                  .isEmpty();
          assertThat(chain.getListOfHeadersStartFrom(chain.getBestBlock().getNumber() + 10, 1))
                  .isEmpty();
      }
  
      @Test
      public void testGetListOfHeadersStartFrom_wGenesisBlock() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle =
                  builder.withValidatorConfiguration("simple").withDefaultAccounts(accounts).build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChain(chain, 12, 2, accounts, MAX_TX_PER_BLOCK);
  
          MiningBlock genesis = chain.getGenesis();
          Block best = chain.getBestBlock();
  
          assertThat(best.getNumber()).isGreaterThan(0L);
          byte[] genesisHash = genesis.getHash();
  
          // expected result with qty >= chain height
          List<ByteArrayWrapper> expectedAll = new ArrayList<>();
          Block block = chain.getBlockByHash(best.getParentHash());
          while (block.getNumber() > 0) {
              expectedAll.add(0, ByteArrayWrapper.wrap(block.getHash()));
              block = chain.getBlockByHash(block.getParentHash());
          }
          expectedAll.add(0, ByteArrayWrapper.wrap(genesis.getHash()));
  
          int qty;
          // get one block
          qty = 1;
          assertStartFrom_expectedOne(chain, genesisHash, 0, qty);
  
          // get list of chain height from genesis block
          qty = (int) best.getNumber(); // chain height
          assertStartFrom(chain, 0, qty, expectedAll);
  
          // get list of half from genesis block
          qty = (int) (best.getNumber() / 2);
  
          List<ByteArrayWrapper> expectedHalf = new ArrayList<>();
          block = chain.getBlockByNumber(qty - 1);
          for (int i = 0; i < qty; i++) {
              expectedHalf.add(0, ByteArrayWrapper.wrap(block.getHash()));
              block = chain.getBlockByHash(block.getParentHash());
          }
          assertStartFrom(chain, 0, qty, expectedHalf);
  
          // get list of 20 from genesis
          qty = 24; // larger than block height
          expectedAll.add(ByteArrayWrapper.wrap(best.getHash()));
          assertStartFrom(chain, 0, qty, expectedAll);
  
          // get list of -10 from genesis block
          qty = -10; // negative value
          assertThat(chain.getListOfHeadersStartFrom(0, qty)).isEmpty();
  
          // get list of 0 from genesis block
          qty = 0; // zero blocks requested
          assertThat(chain.getListOfHeadersStartFrom(0, qty)).isEmpty();
      }
  
      @Test
      public void testGetListOfHeadersStartFrom_wBestBlock() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle =
                  builder.withValidatorConfiguration("simple").withDefaultAccounts(accounts).build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChain(chain, 6, 2, accounts, MAX_TX_PER_BLOCK);
  
          Block best = chain.getBestBlock();
          byte[] bestHash = best.getHash();
          long bestNumber = best.getNumber();
  
          int qty;
          // get one block
          qty = 1;
          assertStartFrom_expectedOne(chain, bestHash, bestNumber, qty);
  
          // get list of 10 from genesis
          qty = 10; // larger than block height
          assertStartFrom_expectedOne(chain, bestHash, bestNumber, qty);
  
          // get list of -10 from genesis
          qty = -10; // negative value
          assertThat(chain.getListOfHeadersStartFrom(bestNumber, qty)).isEmpty();
  
          // get list of 0 from genesis
          qty = 0; // zero blocks requested
          assertThat(chain.getListOfHeadersStartFrom(bestNumber, qty)).isEmpty();
      }
  
      public static void assertStartFrom_expectedOne(
              StandaloneBlockchain chain, byte[] hash, long number, int qty) {
          List<BlockHeader> hashes = chain.getListOfHeadersStartFrom(number, qty);
          assertThat(hashes.size()).isEqualTo(1);
          assertThat(hashes.get(0).getHash()).isEqualTo(hash);
      }
  
      public static void assertStartFrom(
              StandaloneBlockchain chain, long number, int qty, List<ByteArrayWrapper> expected) {
          List<ByteArrayWrapper> hashes = new ArrayList<>();
          chain.getListOfHeadersStartFrom(number, qty)
                  .forEach(h -> hashes.add(ByteArrayWrapper.wrap(h.getHash())));
          assertThat(hashes.size()).isEqualTo(expected.size());
          assertThat(hashes).isEqualTo(expected);
      }
  
      @Test
      public void testFindMissingAncestor_withCompleteChain() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle = builder.withValidatorConfiguration("simple").build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChainWithoutTransactions(chain, 2, 1);
  
          Block best = chain.getBestBlock();
  
          assertThat(chain.findMissingAncestor(best)).isNull();
      }
  
      @Test
      public void testFindMissingAncestor_withFirstMissing() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle = builder.withValidatorConfiguration("simple").build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChainWithoutTransactions(chain, 2, 1);
  
          Block best = chain.getBestBlock();
          byte[] bestHash = best.getHash();
  
          // delete the block from the db
          ((MockDB) chain.getRepository().getBlockDatabase()).deleteAndCommit(bestHash);
  
          Pair<ByteArrayWrapper, Long> pair = chain.findMissingAncestor(best);
          assertThat(pair.getLeft()).isEqualTo(ByteArrayWrapper.wrap(bestHash));
          assertThat(pair.getRight()).isEqualTo(best.getNumber());
      }
  
      @Test
      public void testFindMissingAncestor_withParentMissing() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle = builder.withValidatorConfiguration("simple").build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChainWithoutTransactions(chain, 2, 1);
  
          Block best = chain.getBestBlock();
          byte[] parentHash = best.getParentHash();
  
          // delete the block from the db
          ((MockDB) chain.getRepository().getBlockDatabase()).deleteAndCommit(parentHash);
  
          Pair<ByteArrayWrapper, Long> pair = chain.findMissingAncestor(best);
          assertThat(pair.getLeft()).isEqualTo(ByteArrayWrapper.wrap(parentHash));
          assertThat(pair.getRight()).isEqualTo(best.getNumber() - 1);
      }
  
      @Test
      public void tryFastImport_withNullBlock() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle = builder.withValidatorConfiguration("simple").build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChainWithoutTransactions(chain, 1, 1);
  
          assertThat(chain.tryFastImport(null)).isEqualTo(FastImportResult.INVALID_BLOCK);
      }
  
      @Test
      public void tryFastImport_withIncorrectTimestamp() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle = builder.withValidatorConfiguration("simple").build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChainWithoutTransactions(chain, 1, 1);
  
          Block parent = chain.getBestBlock();
          MiningBlockHeader.Builder headerBuilder =
                  MiningBlockHeader.Builder.newInstance()
                          .withParentHash(parent.getHash())
                          .withCoinbase(parent.getCoinbase())
                          .withNumber(parent.getNumber() + 1)
                          .withTimestamp(2 * System.currentTimeMillis())
                          .withExtraData(parent.getExtraData())
                          .withDefaultTxTrieRoot()
                          .withEnergyLimit(parent.getNrgLimit())
                          .withDefaultSolution()
                          .withDefaultNonce()
                          .withDefaultDifficulty()
                          .withDefaultLogsBloom()
                          .withDefaultReceiptTrieRoot()
                          .withDefaultStateRoot();
  
          MiningBlock block = new MiningBlock(headerBuilder.build(), Collections.emptyList());
  
          assertThat(chain.tryFastImport(block)).isEqualTo(FastImportResult.INVALID_BLOCK);
      }
  
      @Test
      public void tryFastImport_withKnownBlock() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle = builder.withValidatorConfiguration("simple").build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChainWithoutTransactions(chain, 2, 1);
          Block best = chain.getBestBlock();
  
          assertThat(chain.tryFastImport(best)).isEqualTo(FastImportResult.KNOWN);
      }
  
      @Test
      public void tryFastImport_withNoChild() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle = builder.withValidatorConfiguration("simple").build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChainWithoutTransactions(chain, 2, 1);
          Block best = chain.getBestBlock();
  
          // delete the block from the db
          ((MockDB) chain.getRepository().getBlockDatabase()).deleteAndCommit(best.getHash());
  
          assertThat(chain.tryFastImport(best)).isEqualTo(FastImportResult.NO_CHILD);
      }
  
      @Test
      public void tryFastImport_withImported() {
          StandaloneBlockchain.Builder builder = new StandaloneBlockchain.Builder();
          StandaloneBlockchain.Bundle bundle = builder.withValidatorConfiguration("simple").build();
  
          StandaloneBlockchain chain = bundle.bc;
  
          // populate chain at random
          generateRandomChainWithoutTransactions(chain, 3, 1);
          Block best = chain.getBestBlock();
  
          // save then delete the block from the db
          Block block = chain.getBlockByHash(best.getParentHash());
          ((MockDB) chain.getRepository().getBlockDatabase()).deleteAndCommit(best.getParentHash());
  
          assertThat(chain.tryFastImport(block)).isEqualTo(FastImportResult.IMPORTED);
          assertThat(chain.getRepository().getBlockDatabase().get(best.getParentHash()))
                  .isNotEqualTo(Optional.empty());
      }
  }
  