/**
  * Returns the set of contiguous blocks between 'higher' and 'lower'. Higher is included, lower is
  * not.
  */
 private static LinkedList<StoredBlock> getPartialChain(
     StoredBlock higher, StoredBlock lower, BlockStore store) throws BlockStoreException {
   checkArgument(higher.getHeight() > lower.getHeight(), "higher and lower are reversed");
   LinkedList<StoredBlock> results = new LinkedList<StoredBlock>();
   StoredBlock cursor = higher;
   while (true) {
     results.add(cursor);
     cursor = checkNotNull(cursor.getPrev(store), "Ran off the end of the chain");
     if (cursor.equals(lower)) break;
   }
   return results;
 }
예제 #2
0
 /**
  * Returns the set of contiguous blocks between 'higher' and 'lower'. Higher is included, lower is
  * not.
  */
 private List<StoredBlock> getPartialChain(StoredBlock higher, StoredBlock lower)
     throws BlockStoreException {
   assert higher.getHeight() > lower.getHeight();
   LinkedList<StoredBlock> results = new LinkedList<StoredBlock>();
   StoredBlock cursor = higher;
   while (true) {
     results.add(cursor);
     cursor = cursor.getPrev(blockStore);
     assert cursor != null : "Ran off the end of the chain";
     if (cursor.equals(lower)) break;
   }
   return results;
 }
  private void putUpdateStoredBlock(StoredBlock storedBlock, boolean wasUndoable)
      throws SQLException {
    try {
      PreparedStatement s =
          conn.get()
              .prepareStatement(
                  "INSERT INTO headers(hash, chainWork, height, header, wasUndoable)"
                      + " VALUES(?, ?, ?, ?, ?)");
      // We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes
      byte[] hashBytes = new byte[28];
      System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 3, hashBytes, 0, 28);
      s.setBytes(1, hashBytes);
      s.setBytes(2, storedBlock.getChainWork().toByteArray());
      s.setInt(3, storedBlock.getHeight());
      s.setBytes(4, storedBlock.getHeader().unsafeRimbitSerialize());
      s.setBoolean(5, wasUndoable);
      s.executeUpdate();
      s.close();
    } catch (SQLException e) {
      // It is possible we try to add a duplicate StoredBlock if we upgraded
      // In that case, we just update the entry to mark it wasUndoable
      if (!(e.getSQLState().equals(POSTGRES_DUPLICATE_KEY_ERROR_CODE)) || !wasUndoable) throw e;

      PreparedStatement s =
          conn.get().prepareStatement("UPDATE headers SET wasUndoable=? WHERE hash=?");
      s.setBoolean(1, true);
      // We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes
      byte[] hashBytes = new byte[28];
      System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 3, hashBytes, 0, 28);
      s.setBytes(2, hashBytes);
      s.executeUpdate();
      s.close();
    }
  }
 /**
  * Returns an estimate of when the given block will be reached, assuming a perfect 10 minute
  * average for each block. This is useful for turning transaction lock times into human readable
  * times. Note that a height in the past will still be estimated, even though the time of solving
  * is actually known (we won't scan backwards through the chain to obtain the right answer).
  */
 public Date estimateBlockTime(int height) {
   synchronized (chainHeadLock) {
     long offset = height - chainHead.getHeight();
     long headTime = chainHead.getHeader().getTimeSeconds();
     long estimated =
         (headTime * 1000) + (1000L * 30L * 1L * offset); // 2 blocks per minute
     return new Date(estimated);
   }
 }
 public void setVerifiedChainHead(StoredBlock chainHead) throws BlockStoreException {
   Sha256Hash hash = chainHead.getHeader().getHash();
   this.verifiedChainHeadHash = hash;
   this.verifiedChainHeadBlock = chainHead;
   maybeConnect();
   try {
     PreparedStatement s =
         conn.get().prepareStatement("UPDATE settings SET value = ? WHERE name = ?");
     s.setString(2, VERIFIED_CHAIN_HEAD_SETTING);
     s.setBytes(1, hash.getBytes());
     s.executeUpdate();
     s.close();
   } catch (SQLException ex) {
     throw new BlockStoreException(ex);
   }
   if (this.chainHeadBlock.getHeight() < chainHead.getHeight()) setChainHead(chainHead);
   removeUndoableBlocksWhereHeightIsLessThan(chainHead.getHeight() - fullStoreDepth);
 }
 /** Constructs a BlockChain connected to the given list of listeners (eg, wallets) and a store. */
 public AbstractBlockChain(
     Context context, List<BlockChainListener> listeners, BlockStore blockStore)
     throws BlockStoreException {
   this.blockStore = blockStore;
   chainHead = blockStore.getChainHead();
   log.info("chain head is at height {}:\n{}", chainHead.getHeight(), chainHead.getHeader());
   this.params = context.getParams();
   this.listeners = new CopyOnWriteArrayList<ListenerRegistration<BlockChainListener>>();
   for (BlockChainListener l : listeners) addListener(l, Threading.SAME_THREAD);
 }
예제 #7
0
 /**
  * Locates the point in the chain at which newStoredBlock and chainHead diverge. Returns null if
  * no split point was found (ie they are part of the same chain).
  */
 private StoredBlock findSplit(StoredBlock newChainHead, StoredBlock chainHead)
     throws BlockStoreException {
   StoredBlock currentChainCursor = chainHead;
   StoredBlock newChainCursor = newChainHead;
   // Loop until we find the block both chains have in common. Example:
   //
   //    A -> B -> C -> D
   //         \--> E -> F -> G
   //
   // findSplit will return block B. chainHead = D and newChainHead = G.
   while (!currentChainCursor.equals(newChainCursor)) {
     if (currentChainCursor.getHeight() > newChainCursor.getHeight()) {
       currentChainCursor = currentChainCursor.getPrev(blockStore);
       assert currentChainCursor != null : "Attempt to follow an orphan chain";
     } else {
       newChainCursor = newChainCursor.getPrev(blockStore);
       assert newChainCursor != null : "Attempt to follow an orphan chain";
     }
   }
   return currentChainCursor;
 }
 /**
  * Locates the point in the chain at which newStoredBlock and chainHead diverge. Returns null if
  * no split point was found (ie they are not part of the same chain). Returns newChainHead or
  * chainHead if they don't actually diverge but are part of the same chain.
  */
 private static StoredBlock findSplit(
     StoredBlock newChainHead, StoredBlock oldChainHead, BlockStore store)
     throws BlockStoreException {
   StoredBlock currentChainCursor = oldChainHead;
   StoredBlock newChainCursor = newChainHead;
   // Loop until we find the block both chains have in common. Example:
   //
   //    A -> B -> C -> D
   //         \--> E -> F -> G
   //
   // findSplit will return block B. oldChainHead = D and newChainHead = G.
   while (!currentChainCursor.equals(newChainCursor)) {
     if (currentChainCursor.getHeight() > newChainCursor.getHeight()) {
       currentChainCursor = currentChainCursor.getPrev(store);
       checkNotNull(currentChainCursor, "Attempt to follow an orphan chain");
     } else {
       newChainCursor = newChainCursor.getPrev(store);
       checkNotNull(newChainCursor, "Attempt to follow an orphan chain");
     }
   }
   return currentChainCursor;
 }
예제 #9
0
  private void connectBlock(
      StoredBlock newStoredBlock, StoredBlock storedPrev, List<Transaction> newTransactions)
      throws BlockStoreException, VerificationException {
    if (storedPrev.equals(chainHead)) {
      // This block connects to the best known block, it is a normal continuation of the system.
      setChainHead(newStoredBlock);
      log.trace("Chain is now {} blocks high", chainHead.getHeight());
      if (newTransactions != null)
        sendTransactionsToWallet(newStoredBlock, NewBlockType.BEST_CHAIN, newTransactions);
    } else {
      // This block connects to somewhere other than the top of the best known chain. We treat these
      // differently.
      //
      // Note that we send the transactions to the wallet FIRST, even if we're about to re-organize
      // this block
      // to become the new best chain head. This simplifies handling of the re-org in the Wallet
      // class.
      boolean haveNewBestChain = newStoredBlock.moreWorkThan(chainHead);
      if (haveNewBestChain) {
        log.info("Block is causing a re-organize");
      } else {
        StoredBlock splitPoint = findSplit(newStoredBlock, chainHead);
        String splitPointHash = splitPoint != null ? splitPoint.getHeader().getHashAsString() : "?";
        log.info(
            "Block forks the chain at {}, but it did not cause a reorganize:\n{}",
            splitPointHash,
            newStoredBlock);
      }

      // We may not have any transactions if we received only a header. That never happens today but
      // will in
      // future when getheaders is used as an optimization.
      if (newTransactions != null) {
        sendTransactionsToWallet(newStoredBlock, NewBlockType.SIDE_CHAIN, newTransactions);
      }

      if (haveNewBestChain) handleNewBestChain(newStoredBlock);
    }
  }
예제 #10
0
 /**
  * Called as part of connecting a block when the new block results in a different chain having
  * higher total work.
  */
 private void handleNewBestChain(StoredBlock newChainHead)
     throws BlockStoreException, VerificationException {
   // This chain has overtaken the one we currently believe is best. Reorganize is required.
   //
   // Firstly, calculate the block at which the chain diverged. We only need to examine the
   // chain from beyond this block to find differences.
   StoredBlock splitPoint = findSplit(newChainHead, chainHead);
   log.info("Re-organize after split at height {}", splitPoint.getHeight());
   log.info("Old chain head: {}", chainHead.getHeader().getHashAsString());
   log.info("New chain head: {}", newChainHead.getHeader().getHashAsString());
   log.info("Split at block: {}", splitPoint.getHeader().getHashAsString());
   // Then build a list of all blocks in the old part of the chain and the new part.
   List<StoredBlock> oldBlocks = getPartialChain(chainHead, splitPoint);
   List<StoredBlock> newBlocks = getPartialChain(newChainHead, splitPoint);
   // Now inform the wallet. This is necessary so the set of currently active transactions (that we
   // can spend)
   // can be updated to take into account the re-organize. We might also have received new coins we
   // didn't have
   // before and our previous spends might have been undone.
   wallet.reorganize(oldBlocks, newBlocks);
   // Update the pointer to the best known block.
   setChainHead(newChainHead);
 }
예제 #11
0
 /**
  * Called as part of connecting a block when the new block results in a different chain having
  * higher total work.
  *
  * <p>if (shouldVerifyTransactions) Either newChainHead needs to be in the block store as a
  * FullStoredBlock, or (block != null && block.transactions != null)
  */
 private void handleNewBestChain(
     StoredBlock storedPrev, StoredBlock newChainHead, Block block, boolean expensiveChecks)
     throws BlockStoreException, VerificationException, PrunedException {
   checkState(lock.isHeldByCurrentThread());
   // This chain has overtaken the one we currently believe is best. Reorganize is required.
   //
   // Firstly, calculate the block at which the chain diverged. We only need to examine the
   // chain from beyond this block to find differences.
   StoredBlock head = getChainHead();
   final StoredBlock splitPoint = findSplit(newChainHead, head, blockStore);
   log.info("Re-organize after split at height {}", splitPoint.getHeight());
   log.info("Old chain head: {}", head.getHeader().getHashAsString());
   log.info("New chain head: {}", newChainHead.getHeader().getHashAsString());
   log.info("Split at block: {}", splitPoint.getHeader().getHashAsString());
   // Then build a list of all blocks in the old part of the chain and the new part.
   final LinkedList<StoredBlock> oldBlocks = getPartialChain(head, splitPoint, blockStore);
   final LinkedList<StoredBlock> newBlocks = getPartialChain(newChainHead, splitPoint, blockStore);
   // Disconnect each transaction in the previous main chain that is no longer in the new main
   // chain
   StoredBlock storedNewHead = splitPoint;
   if (shouldVerifyTransactions()) {
     for (StoredBlock oldBlock : oldBlocks) {
       try {
         disconnectTransactions(oldBlock);
       } catch (PrunedException e) {
         // We threw away the data we need to re-org this deep! We need to go back to a peer with
         // full
         // block contents and ask them for the relevant data then rebuild the indexs. Or we could
         // just
         // give up and ask the human operator to help get us unstuck (eg, rescan from the genesis
         // block).
         // TODO: Retry adding this block when we get a block with hash e.getHash()
         throw e;
       }
     }
     StoredBlock cursor;
     // Walk in ascending chronological order.
     for (Iterator<StoredBlock> it = newBlocks.descendingIterator(); it.hasNext(); ) {
       cursor = it.next();
       Block cursorBlock = cursor.getHeader();
       if (expensiveChecks
           && cursorBlock.getTimeSeconds()
               <= getMedianTimestampOfRecentBlocks(cursor.getPrev(blockStore), blockStore))
         throw new VerificationException("Block's timestamp is too early during reorg");
       TransactionOutputChanges txOutChanges;
       if (cursor != newChainHead || block == null) txOutChanges = connectTransactions(cursor);
       else txOutChanges = connectTransactions(newChainHead.getHeight(), block);
       storedNewHead = addToBlockStore(storedNewHead, cursorBlock.cloneAsHeader(), txOutChanges);
     }
   } else {
     // (Finally) write block to block store
     storedNewHead = addToBlockStore(storedPrev, newChainHead.getHeader());
   }
   // Now inform the listeners. This is necessary so the set of currently active transactions (that
   // we can spend)
   // can be updated to take into account the re-organize. We might also have received new coins we
   // didn't have
   // before and our previous spends might have been undone.
   for (final ListenerRegistration<BlockChainListener> registration : listeners) {
     if (registration.executor == Threading.SAME_THREAD) {
       // Short circuit the executor so we can propagate any exceptions.
       // TODO: Do we really need to do this or should it be irrelevant?
       registration.listener.reorganize(splitPoint, oldBlocks, newBlocks);
     } else {
       registration.executor.execute(
           new Runnable() {
             @Override
             public void run() {
               try {
                 registration.listener.reorganize(splitPoint, oldBlocks, newBlocks);
               } catch (VerificationException e) {
                 log.error("Block chain listener threw exception during reorg", e);
               }
             }
           });
     }
   }
   // Update the pointer to the best known block.
   setChainHead(storedNewHead);
 }
예제 #12
0
  // expensiveChecks enables checks that require looking at blocks further back in the chain
  // than the previous one when connecting (eg median timestamp check)
  // It could be exposed, but for now we just set it to shouldVerifyTransactions()
  private void connectBlock(
      final Block block,
      StoredBlock storedPrev,
      boolean expensiveChecks,
      @Nullable final List<Sha256Hash> filteredTxHashList,
      @Nullable final Map<Sha256Hash, Transaction> filteredTxn)
      throws BlockStoreException, VerificationException, PrunedException {
    checkState(lock.isHeldByCurrentThread());
    boolean filtered = filteredTxHashList != null && filteredTxn != null;
    // Check that we aren't connecting a block that fails a checkpoint check
    if (!params.passesCheckpoint(storedPrev.getHeight() + 1, block.getHash()))
      throw new VerificationException(
          "Block failed checkpoint lockin at " + (storedPrev.getHeight() + 1));
    if (shouldVerifyTransactions()) {
      checkNotNull(block.transactions);
      for (Transaction tx : block.transactions)
        if (!tx.isFinal(storedPrev.getHeight() + 1, block.getTimeSeconds()))
          throw new VerificationException("Block contains non-final transaction");
    }

    StoredBlock head = getChainHead();
    if (storedPrev.equals(head)) {
      if (filtered && filteredTxn.size() > 0) {
        log.debug(
            "Block {} connects to top of best chain with {} transaction(s) of which we were sent {}",
            block.getHashAsString(),
            filteredTxHashList.size(),
            filteredTxn.size());
        for (Sha256Hash hash : filteredTxHashList) log.debug("  matched tx {}", hash);
      }
      if (expensiveChecks
          && block.getTimeSeconds() <= getMedianTimestampOfRecentBlocks(head, blockStore))
        throw new VerificationException("Block's timestamp is too early");

      // This block connects to the best known block, it is a normal continuation of the system.
      TransactionOutputChanges txOutChanges = null;
      if (shouldVerifyTransactions())
        txOutChanges = connectTransactions(storedPrev.getHeight() + 1, block);
      StoredBlock newStoredBlock =
          addToBlockStore(
              storedPrev, block.transactions == null ? block : block.cloneAsHeader(), txOutChanges);
      setChainHead(newStoredBlock);
      log.debug("Chain is now {} blocks high, running listeners", newStoredBlock.getHeight());
      informListenersForNewBlock(
          block, NewBlockType.BEST_CHAIN, filteredTxHashList, filteredTxn, newStoredBlock);
    } else {
      // This block connects to somewhere other than the top of the best known chain. We treat these
      // differently.
      //
      // Note that we send the transactions to the wallet FIRST, even if we're about to re-organize
      // this block
      // to become the new best chain head. This simplifies handling of the re-org in the Wallet
      // class.
      StoredBlock newBlock = storedPrev.build(block);
      boolean haveNewBestChain = newBlock.moreWorkThan(head);
      if (haveNewBestChain) {
        log.info("Block is causing a re-organize");
      } else {
        StoredBlock splitPoint = findSplit(newBlock, head, blockStore);
        if (splitPoint != null && splitPoint.equals(newBlock)) {
          // newStoredBlock is a part of the same chain, there's no fork. This happens when we
          // receive a block
          // that we already saw and linked into the chain previously, which isn't the chain head.
          // Re-processing it is confusing for the wallet so just skip.
          log.warn(
              "Saw duplicated block in main chain at height {}: {}",
              newBlock.getHeight(),
              newBlock.getHeader().getHash());
          return;
        }
        if (splitPoint == null) {
          // This should absolutely never happen
          // (lets not write the full block to disk to keep any bugs which allow this to happen
          //  from writing unreasonable amounts of data to disk)
          throw new VerificationException("Block forks the chain but splitPoint is null");
        } else {
          // We aren't actually spending any transactions (yet) because we are on a fork
          addToBlockStore(storedPrev, block);
          int splitPointHeight = splitPoint.getHeight();
          String splitPointHash = splitPoint.getHeader().getHashAsString();
          log.info(
              "Block forks the chain at height {}/block {}, but it did not cause a reorganize:\n{}",
              splitPointHeight,
              splitPointHash,
              newBlock.getHeader().getHashAsString());
        }
      }

      // We may not have any transactions if we received only a header, which can happen during fast
      // catchup.
      // If we do, send them to the wallet but state that they are on a side chain so it knows not
      // to try and
      // spend them until they become activated.
      if (block.transactions != null || filtered) {
        informListenersForNewBlock(
            block, NewBlockType.SIDE_CHAIN, filteredTxHashList, filteredTxn, newBlock);
      }

      if (haveNewBestChain) handleNewBestChain(storedPrev, newBlock, block, expensiveChecks);
    }
  }
  public void put(StoredBlock storedBlock, StoredUndoableBlock undoableBlock)
      throws BlockStoreException {
    maybeConnect();
    // We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes
    byte[] hashBytes = new byte[28];
    System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 3, hashBytes, 0, 28);
    int height = storedBlock.getHeight();
    byte[] transactions = null;
    byte[] txOutChanges = null;
    try {
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      if (undoableBlock.getTxOutChanges() != null) {
        undoableBlock.getTxOutChanges().serializeToStream(bos);
        txOutChanges = bos.toByteArray();
      } else {
        int numTxn = undoableBlock.getTransactions().size();
        bos.write((int) (0xFF & (numTxn >> 0)));
        bos.write((int) (0xFF & (numTxn >> 8)));
        bos.write((int) (0xFF & (numTxn >> 16)));
        bos.write((int) (0xFF & (numTxn >> 24)));
        for (Transaction tx : undoableBlock.getTransactions()) tx.rimbitSerialize(bos);
        transactions = bos.toByteArray();
      }
      bos.close();
    } catch (IOException e) {
      throw new BlockStoreException(e);
    }

    try {
      if (log.isDebugEnabled())
        log.debug("Looking for undoable block with hash: " + Utils.bytesToHexString(hashBytes));

      PreparedStatement findS =
          conn.get().prepareStatement("select 1 from undoableBlocks where hash = ?");
      findS.setBytes(1, hashBytes);

      ResultSet rs = findS.executeQuery();
      if (rs.next()) {
        // We already have this output, update it.
        findS.close();

        // Postgres insert-or-updates are very complex (and finnicky).  This level of transaction
        // isolation
        // seems to work for rimbitj
        PreparedStatement s =
            conn.get()
                .prepareStatement(
                    "UPDATE undoableBlocks SET txOutChanges=?, transactions=?" + " WHERE hash = ?");
        s.setBytes(3, hashBytes);

        if (log.isDebugEnabled())
          log.debug("Updating undoable block with hash: " + Utils.bytesToHexString(hashBytes));

        if (transactions == null) {
          s.setBytes(1, txOutChanges);
          s.setNull(2, Types.BINARY);
        } else {
          s.setNull(1, Types.BINARY);
          s.setBytes(2, transactions);
        }
        s.executeUpdate();
        s.close();

        return;
      }

      PreparedStatement s =
          conn.get()
              .prepareStatement(
                  "INSERT INTO undoableBlocks(hash, height, txOutChanges, transactions)"
                      + " VALUES(?, ?, ?, ?)");
      s.setBytes(1, hashBytes);
      s.setInt(2, height);

      if (log.isDebugEnabled())
        log.debug(
            "Inserting undoable block with hash: "
                + Utils.bytesToHexString(hashBytes)
                + " at height "
                + height);

      if (transactions == null) {
        s.setBytes(3, txOutChanges);
        s.setNull(4, Types.BINARY);
      } else {
        s.setNull(3, Types.BINARY);
        s.setBytes(4, transactions);
      }
      s.executeUpdate();
      s.close();
      try {
        putUpdateStoredBlock(storedBlock, true);
      } catch (SQLException e) {
        throw new BlockStoreException(e);
      }
    } catch (SQLException e) {
      if (!e.getSQLState().equals(POSTGRES_DUPLICATE_KEY_ERROR_CODE))
        throw new BlockStoreException(e);
    }
  }
예제 #14
0
  /** Throws an exception if the blocks difficulty is not correct. */
  private void checkDifficultyTransitions(StoredBlock storedPrev, StoredBlock storedNext)
      throws BlockStoreException, VerificationException {
    Block prev = storedPrev.getHeader();
    Block next = storedNext.getHeader();
    // Is this supposed to be a difficulty transition point?
    if ((storedPrev.getHeight() + 1) % params.interval != 0) {
      // No ... so check the difficulty didn't actually change.
      if (next.getDifficultyTarget() != prev.getDifficultyTarget())
        throw new VerificationException(
            "Unexpected change in difficulty at height "
                + storedPrev.getHeight()
                + ": "
                + Long.toHexString(next.getDifficultyTarget())
                + " vs "
                + Long.toHexString(prev.getDifficultyTarget()));
      return;
    }

    // We need to find a block far back in the chain. It's OK that this is expensive because it only
    // occurs every
    // two weeks after the initial block chain download.
    long now = System.currentTimeMillis();
    StoredBlock cursor = blockStore.get(prev.getHash());
    for (int i = 0; i < params.interval - 1; i++) {
      if (cursor == null) {
        // This should never happen. If it does, it means we are following an incorrect or busted
        // chain.
        throw new VerificationException(
            "Difficulty transition point but we did not find a way back to the genesis block.");
      }
      cursor = blockStore.get(cursor.getHeader().getPrevBlockHash());
    }
    log.info("Difficulty transition traversal took {}msec", System.currentTimeMillis() - now);

    Block blockIntervalAgo = cursor.getHeader();
    int timespan = (int) (prev.getTime() - blockIntervalAgo.getTime());
    // Limit the adjustment step.
    if (timespan < params.targetTimespan / 4) timespan = params.targetTimespan / 4;
    if (timespan > params.targetTimespan * 4) timespan = params.targetTimespan * 4;

    BigInteger newDifficulty = Utils.decodeCompactBits(blockIntervalAgo.getDifficultyTarget());
    newDifficulty = newDifficulty.multiply(BigInteger.valueOf(timespan));
    newDifficulty = newDifficulty.divide(BigInteger.valueOf(params.targetTimespan));

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

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

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

    if (newDifficulty.compareTo(receivedDifficulty) != 0)
      throw new VerificationException(
          "Network provided difficulty bits do not match what was calculated: "
              + receivedDifficulty.toString(16)
              + " vs "
              + newDifficulty.toString(16));
  }