@Override
 protected int backTill(NetworkParameters params, StoredBlock prevBlock, Block added) {
   final int height = prevBlock.getHeight();
   final int interval = params.getInterval(prevBlock.getHeader(), height);
   // Dash: This fixes an issue where a 51% attack can change difficulty at will.
   // Go back the full period unless it's the first retarget after genesis.
   // Code courtesy of Art Forz.
   return (height + 1 != interval) ? interval : height;
 }
  @Nullable
  Sha256Hash getHashByHeight(int height) {
    Preconditions.checkArgument(height >= 0, "Height mustn't be negative");

    final AbstractBlockChain blockChain = linearExtension.getBlockChain();
    StoredBlock currentBlock = blockChain.getChainHead();
    if (currentBlock == null || currentBlock.getHeight() < height) {
      return null;
    }

    final BlockStore blockStore = blockChain.getBlockStore();
    try {
      while (currentBlock.getHeight() > height) {
        currentBlock = currentBlock.getPrev(blockStore);
        if (currentBlock == null) return null;
      }
      return currentBlock.getHeader().getHash();
    } catch (BlockStoreException ex) {
      log.error("Error while descending the chain", ex);
      return null;
    }
  }
  private static void verifyDifficulty(
      StoredBlock prevBlock, Block added, BigInteger calcDiff, NetworkParameters params) {
    if (calcDiff.compareTo(params.getMaxTarget()) > 0) {
      log.info("Difficulty hit proof of work limit: {}", calcDiff.toString(16));
      calcDiff = params.getMaxTarget();
    }

    int accuracyBytes = (int) (added.getDifficultyTarget() >>> 24) - 3;
    final BigInteger receivedDifficulty = added.getDifficultyTargetAsInteger();

    // The calculated difficulty is to a higher precision than received, so reduce here.
    final BigInteger mask = BigInteger.valueOf(0xFFFFFFL).shiftLeft(accuracyBytes * 8);
    calcDiff = calcDiff.and(mask);

    if (CoinDefinition.TEST_NETWORK_STANDARD.equals(params.getStandardNetworkId())) {
      if (calcDiff.compareTo(receivedDifficulty) != 0) {
        throw new VerificationException(
            "Network provided difficulty bits do not match what was calculated: "
                + receivedDifficulty.toString(16)
                + " vs "
                + calcDiff.toString(16));
      }
    } else {
      final int height = prevBlock.getHeight() + 1;
      if (height <= 68589) {
        long nBitsNext = added.getDifficultyTarget();

        long calcDiffBits = (accuracyBytes + 3) << 24;
        calcDiffBits |= calcDiff.shiftRight(accuracyBytes * 8).longValue();

        final double n1 = CommonUtils.convertBitsToDouble(calcDiffBits);
        final double n2 = CommonUtils.convertBitsToDouble(nBitsNext);

        if (Math.abs(n1 - n2) > n1 * 0.2) {
          throw new VerificationException(
              "Network provided difficulty bits do not match what was calculated: "
                  + receivedDifficulty.toString(16)
                  + " vs "
                  + calcDiff.toString(16));
        }
      } else {
        if (calcDiff.compareTo(receivedDifficulty) != 0) {
          throw new VerificationException(
              "Network provided difficulty bits do not match what was calculated: "
                  + receivedDifficulty.toString(16)
                  + " vs "
                  + calcDiff.toString(16));
        }
      }
    }
  }
  @Override
  public void verifyDifficultyTransitions(
      StoredBlock prevBlock, Block added, NetworkParameters params) {
    int diffMode = 1;
    final int heightInc = prevBlock.getHeight() + 1;
    if (CoinDefinition.TEST_NETWORK_STANDARD.equals(params.getStandardNetworkId())) {
      if (heightInc >= 2000) {
        diffMode = 4;
      }
    } else {
      if (heightInc >= 68589) {
        diffMode = 4;
      } else if (heightInc >= 34140) {
        diffMode = 3;
      } else if (heightInc >= 15200) {
        diffMode = 2;
      }
    }

    try {
      switch (diffMode) {
        case 4:
          darkGravityWave3Check(
              prevBlock, added, linearExtension.getBlockChain().getBlockStore(), params);
          return;
        case 1:
          linearExtension.verifyDifficultyTransitions(prevBlock, added, params);
          return;
        case 2:
          kimotoGravityWellCheck(
              prevBlock, added, linearExtension.getBlockChain().getBlockStore(), params);
          return;
        case 3:
          darkGravityWaveCheck(
              prevBlock, added, linearExtension.getBlockChain().getBlockStore(), params);
          return;
        default:
          throw new RuntimeException("Unreachable");
      }
    } catch (BlockStoreException ex) {
      throw new VerificationException(
          "Block store exception during difficulty transitions check", ex);
    }
  }
  private static void darkGravityWave3Check(
      StoredBlock prevBlock, Block added, BlockStore store, NetworkParameters params) {
    StoredBlock blockReading = prevBlock;

    long actualTimespan = 0;
    long lastBlockTime = 0;
    long pastBlocksMin = 24;
    long pastBlocksMax = 24;
    long countBlocks = 0;

    BigInteger pastDifficultyAverage = BigInteger.ZERO;
    BigInteger pastDifficultyAveragePrev = BigInteger.ZERO;

    if (prevBlock == null || prevBlock.getHeight() == 0 || prevBlock.getHeight() < pastBlocksMin) {
      verifyDifficulty(prevBlock, added, params.getMaxTarget(), params);
      return;
    }

    for (int i = 1; blockReading.getHeight() > 0; i++) {
      if (i > pastBlocksMax) {
        break;
      }
      countBlocks++;

      if (countBlocks <= pastBlocksMin) {
        if (countBlocks == 1) {
          pastDifficultyAverage = blockReading.getHeader().getDifficultyTargetAsInteger();
        } else {
          pastDifficultyAverage =
              pastDifficultyAveragePrev
                  .multiply(BigInteger.valueOf(countBlocks))
                  .add(blockReading.getHeader().getDifficultyTargetAsInteger())
                  .divide(BigInteger.valueOf(countBlocks + 1));
        }
        pastDifficultyAveragePrev = pastDifficultyAverage;
      }

      if (lastBlockTime > 0) {
        actualTimespan += (lastBlockTime - blockReading.getHeader().getTimeSeconds());
      }
      lastBlockTime = blockReading.getHeader().getTimeSeconds();

      try {
        blockReading = store.get(blockReading.getHeader().getPrevBlockHash());
        if (blockReading == null) {
          return;
        }
      } catch (BlockStoreException ex) {
        log.warn("Dark gravity wave 3 descended to start of the chain");
        return;
      }
    }

    BigInteger bnNew = pastDifficultyAverage;

    long targetTimespan =
        countBlocks * params.getTargetSpacing(prevBlock.getHeader(), prevBlock.getHeight());

    if (actualTimespan < targetTimespan / 3) {
      actualTimespan = targetTimespan / 3;
    }
    if (actualTimespan > targetTimespan * 3) {
      actualTimespan = targetTimespan * 3;
    }

    // Retarget
    bnNew = bnNew.multiply(BigInteger.valueOf(actualTimespan));
    bnNew = bnNew.divide(BigInteger.valueOf(targetTimespan));

    verifyDifficulty(prevBlock, added, bnNew, params);
  }
  private static void darkGravityWaveCheck(
      StoredBlock prevBlock, Block added, BlockStore store, NetworkParameters params) {
    StoredBlock blockReading = prevBlock;

    long blockTimeAverage = 0;
    long blockTimeAveragePrev = 0;
    long blockTimeCount = 0;
    long blockTimeSum2 = 0;
    long blockTimeCount2 = 0;
    long lastBlockTime = 0;
    long pastBlocksMin = 14;
    long pastBlocksMax = 140;
    long countBlocks = 0;

    BigInteger pastDifficultyAverage = BigInteger.valueOf(0);
    BigInteger pastDifficultyAveragePrev = BigInteger.valueOf(0);

    if (prevBlock == null || prevBlock.getHeight() == 0 || prevBlock.getHeight() < pastBlocksMin) {
      verifyDifficulty(prevBlock, added, params.getMaxTarget(), params);
      return;
    }

    for (int i = 1; blockReading.getHeight() > 0; i++) {
      if (i > pastBlocksMax) {
        break;
      }
      countBlocks++;

      if (countBlocks <= pastBlocksMin) {
        if (countBlocks == 1) {
          pastDifficultyAverage = blockReading.getHeader().getDifficultyTargetAsInteger();
        } else {
          pastDifficultyAverage =
              blockReading
                  .getHeader()
                  .getDifficultyTargetAsInteger()
                  .subtract(pastDifficultyAveragePrev)
                  .divide(BigInteger.valueOf(countBlocks))
                  .add(pastDifficultyAveragePrev);
        }
        pastDifficultyAveragePrev = pastDifficultyAverage;
      }

      if (lastBlockTime > 0) {
        long diff = lastBlockTime - blockReading.getHeader().getTimeSeconds();
        if (blockTimeCount <= pastBlocksMin) {
          blockTimeCount++;

          if (blockTimeCount == 1) {
            blockTimeAverage = diff;
          } else {
            blockTimeAverage =
                (diff - blockTimeAveragePrev) / blockTimeCount + blockTimeAveragePrev;
          }
          blockTimeAveragePrev = blockTimeAverage;
        }
        blockTimeCount2++;
        blockTimeSum2 += diff;
      }
      lastBlockTime = blockReading.getHeader().getTimeSeconds();

      try {
        blockReading = store.get(blockReading.getHeader().getPrevBlockHash());
        if (blockReading == null) {
          return;
        }
      } catch (BlockStoreException ex) {
        log.warn("Dark gravity wave 3 descended to start of the chain");
        return;
      }
    }

    BigInteger bnNew = pastDifficultyAverage;

    if (blockTimeCount != 0 && blockTimeCount2 != 0) {
      double smartAverage =
          ((double) blockTimeAverage) * 0.7
              + ((double) blockTimeSum2 / (double) blockTimeCount2) * 0.3;
      if (smartAverage < 1) smartAverage = 1;

      final int targetSpacing =
          params.getTargetSpacing(prevBlock.getHeader(), prevBlock.getHeight());
      final double shift = targetSpacing / smartAverage;

      final double dCountBlocks = (double) countBlocks;
      double actualTimespan = dCountBlocks * ((double) targetSpacing) / shift;
      double targetTimespan = dCountBlocks * targetSpacing;

      if (actualTimespan < targetTimespan / 3) actualTimespan = targetTimespan / 3;
      if (actualTimespan > targetTimespan * 3) actualTimespan = targetTimespan * 3;

      // Retarget
      bnNew = bnNew.multiply(BigInteger.valueOf((long) actualTimespan));
      bnNew = bnNew.divide(BigInteger.valueOf((long) targetTimespan));
    }

    verifyDifficulty(prevBlock, added, bnNew, params);
  }
  private static void kimotoGravityWellCheck(
      StoredBlock prevBlock, Block added, BlockStore store, NetworkParameters params)
      throws BlockStoreException {
    final long blocksTargetSpacing = (long) (2.5 * 60); // 2.5 minutes
    int timeDaySeconds = 60 * 60 * 24;
    long pastSecondsMin = timeDaySeconds / 40;
    long pastSecondsMax = timeDaySeconds * 7;
    long pastBlocksMin = pastSecondsMin / blocksTargetSpacing;
    long pastBlocksMax = pastSecondsMax / blocksTargetSpacing;

    StoredBlock blockReading = prevBlock;

    long pastBlocksMass = 0;
    long pastRateActualSeconds = 0;
    long pastRateTargetSeconds = 0;
    double pastRateAdjustmentRatio = 1.0f;
    BigInteger pastDifficultyAverage = BigInteger.valueOf(0);
    BigInteger pastDifficultyAveragePrev = BigInteger.valueOf(0);
    double eventHorizonDeviation;
    double eventHorizonDeviationFast;
    double eventHorizonDeviationSlow;

    if (prevBlock == null
        || prevBlock.getHeight() == 0
        || (long) prevBlock.getHeight() < pastBlocksMin) {
      verifyDifficulty(prevBlock, added, params.getMaxTarget(), params);
      return;
    }

    final Block prevHeader = prevBlock.getHeader();
    long latestBlockTime = prevHeader.getTimeSeconds();

    for (int i = 1; blockReading.getHeight() > 0; i++) {
      if (pastBlocksMax > 0 && i > pastBlocksMax) {
        break;
      }
      pastBlocksMass++;

      if (i == 1) {
        pastDifficultyAverage = blockReading.getHeader().getDifficultyTargetAsInteger();
      } else {
        pastDifficultyAverage =
            (blockReading
                    .getHeader()
                    .getDifficultyTargetAsInteger()
                    .subtract(pastDifficultyAveragePrev))
                .divide(BigInteger.valueOf(i))
                .add(pastDifficultyAveragePrev);
      }
      pastDifficultyAveragePrev = pastDifficultyAverage;

      if (blockReading.getHeight() > 646120
          && latestBlockTime < blockReading.getHeader().getTimeSeconds()) {
        // eliminates the ability to go back in time
        latestBlockTime = blockReading.getHeader().getTimeSeconds();
      }

      pastRateActualSeconds =
          prevHeader.getTimeSeconds() - blockReading.getHeader().getTimeSeconds();
      pastRateTargetSeconds = blocksTargetSpacing * pastBlocksMass;
      if (blockReading.getHeight() > 646120) {
        // this should slow down the upward difficulty change
        if (pastRateActualSeconds < 5) {
          pastRateActualSeconds = 5;
        }
      } else {
        if (pastRateActualSeconds < 0) {
          pastRateActualSeconds = 0;
        }
      }
      if (pastRateActualSeconds != 0 && pastRateTargetSeconds != 0) {
        pastRateAdjustmentRatio = (double) pastRateTargetSeconds / pastRateActualSeconds;
      }
      eventHorizonDeviation = 1 + 0.7084 * Math.pow((double) pastBlocksMass / 28.2d, -1.228);
      eventHorizonDeviationFast = eventHorizonDeviation;
      eventHorizonDeviationSlow = 1 / eventHorizonDeviation;

      if (pastBlocksMass >= pastBlocksMin) {
        if (pastRateAdjustmentRatio <= eventHorizonDeviationSlow
            || pastRateAdjustmentRatio >= eventHorizonDeviationFast) {
          break;
        }
      }
      blockReading = store.get(blockReading.getHeader().getPrevBlockHash());
      if (blockReading == null) {
        return;
      }
    }

    BigInteger newDifficulty = pastDifficultyAverage;
    if (pastRateActualSeconds != 0 && pastRateTargetSeconds != 0) {
      newDifficulty = newDifficulty.multiply(BigInteger.valueOf(pastRateActualSeconds));
      newDifficulty = newDifficulty.divide(BigInteger.valueOf(pastRateTargetSeconds));
    }

    if (newDifficulty.compareTo(params.getMaxTarget()) > 0) {
      log.info("Difficulty hit proof of work limit: {}", newDifficulty.toString(16));
      newDifficulty = params.getMaxTarget();
    }

    verifyDifficulty(prevBlock, added, newDifficulty, params);
  }