From e030f1a1f42b9e41a78fc3c7698bbf12733172e3 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Tue, 12 May 2015 20:49:17 +0100 Subject: [PATCH] Moved block difficulty checks into NetworkParameters. --- .../org/bitcoinj/core/AbstractBlockChain.java | 106 +----------------- .../bitcoinj/core/FullPrunedBlockChain.java | 12 +- .../org/bitcoinj/core/NetworkParameters.java | 12 ++ .../params/AbstractBitcoinNetParams.java | 70 ++++++++++++ .../org/bitcoinj/params/TestNet3Params.java | 44 ++++++++ 5 files changed, 134 insertions(+), 110 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/AbstractBlockChain.java b/core/src/main/java/org/bitcoinj/core/AbstractBlockChain.java index 33258b0f..51ba897b 100644 --- a/core/src/main/java/org/bitcoinj/core/AbstractBlockChain.java +++ b/core/src/main/java/org/bitcoinj/core/AbstractBlockChain.java @@ -409,8 +409,9 @@ public abstract class AbstractBlockChain { orphanBlocks.put(block.getHash(), new OrphanBlock(block, filteredTxHashList, filteredTxn)); return false; } else { + checkState(lock.isHeldByCurrentThread()); // It connects to somewhere on the chain. Not necessarily the top of the best known chain. - checkDifficultyTransitions(storedPrev, block); + params.checkDifficultyTransitions(storedPrev, block, blockStore); connectBlock(block, storedPrev, shouldVerifyTransactions(), filteredTxHashList, filteredTxn); } @@ -825,109 +826,6 @@ public abstract class AbstractBlockChain { } while (blocksConnectedThisRound > 0); } - // February 16th 2012 - private static final Date testnetDiffDate = new Date(1329264000000L); - - /** - * Throws an exception if the block's difficulty is not correct. - * - * @throws VerificationException if the block's difficulty is not correct. - */ - protected void checkDifficultyTransitions(StoredBlock storedPrev, Block nextBlock) throws BlockStoreException, VerificationException { - checkState(lock.isHeldByCurrentThread()); - Block prev = storedPrev.getHeader(); - - // Is this supposed to be a difficulty transition point? - if ((storedPrev.getHeight() + 1) % params.getInterval() != 0) { - - // TODO: Refactor this hack after 0.5 is released and we stop supporting deserialization compatibility. - // This should be a method of the NetworkParameters, which should in turn be using singletons and a subclass - // for each network type. Then each network can define its own difficulty transition rules. - if (params.getId().equals(NetworkParameters.ID_TESTNET) && nextBlock.getTime().after(testnetDiffDate)) { - checkTestnetDifficulty(storedPrev, prev, nextBlock); - return; - } - - // No ... so check the difficulty didn't actually change. - if (nextBlock.getDifficultyTarget() != prev.getDifficultyTarget()) - throw new VerificationException("Unexpected change in difficulty at height " + storedPrev.getHeight() + - ": " + Long.toHexString(nextBlock.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.getInterval() - 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()); - } - long elapsed = System.currentTimeMillis() - now; - if (elapsed > 50) - log.info("Difficulty transition traversal took {}msec", elapsed); - - Block blockIntervalAgo = cursor.getHeader(); - int timespan = (int) (prev.getTimeSeconds() - blockIntervalAgo.getTimeSeconds()); - // Limit the adjustment step. - final int targetTimespan = params.getTargetTimespan(); - if (timespan < targetTimespan / 4) - timespan = targetTimespan / 4; - if (timespan > targetTimespan * 4) - timespan = targetTimespan * 4; - - BigInteger newTarget = Utils.decodeCompactBits(prev.getDifficultyTarget()); - newTarget = newTarget.multiply(BigInteger.valueOf(timespan)); - newTarget = newTarget.divide(BigInteger.valueOf(targetTimespan)); - - if (newTarget.compareTo(params.getMaxTarget()) > 0) { - log.info("Difficulty hit proof of work limit: {}", newTarget.toString(16)); - newTarget = params.getMaxTarget(); - } - - int accuracyBytes = (int) (nextBlock.getDifficultyTarget() >>> 24) - 3; - long receivedTargetCompact = nextBlock.getDifficultyTarget(); - - // The calculated difficulty is to a higher precision than received, so reduce here. - BigInteger mask = BigInteger.valueOf(0xFFFFFFL).shiftLeft(accuracyBytes * 8); - newTarget = newTarget.and(mask); - long newTargetCompact = Utils.encodeCompactBits(newTarget); - - if (newTargetCompact != receivedTargetCompact) - throw new VerificationException("Network provided difficulty bits do not match what was calculated: " + - newTargetCompact + " vs " + receivedTargetCompact); - } - - private void checkTestnetDifficulty(StoredBlock storedPrev, Block prev, Block next) throws VerificationException, BlockStoreException { - checkState(lock.isHeldByCurrentThread()); - // After 15th February 2012 the rules on the testnet change to avoid people running up the difficulty - // and then leaving, making it too hard to mine a block. On non-difficulty transition points, easy - // blocks are allowed if there has been a span of 20 minutes without one. - final long timeDelta = next.getTimeSeconds() - prev.getTimeSeconds(); - // There is an integer underflow bug in bitcoin-qt that means mindiff blocks are accepted when time - // goes backwards. - if (timeDelta >= 0 && timeDelta <= NetworkParameters.TARGET_SPACING * 2) { - // Walk backwards until we find a block that doesn't have the easiest proof of work, then check - // that difficulty is equal to that one. - StoredBlock cursor = storedPrev; - while (!cursor.getHeader().equals(params.getGenesisBlock()) && - cursor.getHeight() % params.getInterval() != 0 && - cursor.getHeader().getDifficultyTargetAsInteger().equals(params.getMaxTarget())) - cursor = cursor.getPrev(blockStore); - BigInteger cursorTarget = cursor.getHeader().getDifficultyTargetAsInteger(); - BigInteger newTarget = next.getDifficultyTargetAsInteger(); - if (!cursorTarget.equals(newTarget)) - throw new VerificationException("Testnet block transition that is not allowed: " + - Long.toHexString(cursor.getHeader().getDifficultyTarget()) + " vs " + - Long.toHexString(next.getDifficultyTarget())); - } - } - /** * Returns true if any connected wallet considers any transaction in the block to be relevant. */ diff --git a/core/src/main/java/org/bitcoinj/core/FullPrunedBlockChain.java b/core/src/main/java/org/bitcoinj/core/FullPrunedBlockChain.java index 4419ea41..e7b0a822 100644 --- a/core/src/main/java/org/bitcoinj/core/FullPrunedBlockChain.java +++ b/core/src/main/java/org/bitcoinj/core/FullPrunedBlockChain.java @@ -297,12 +297,12 @@ public class FullPrunedBlockChain extends AbstractBlockChain { } // All values were already checked for being non-negative (as it is verified in Transaction.verify()) // but we check again here just for defence in depth. Transactions with zero output value are OK. - if (valueOut.signum() < 0 || valueOut.compareTo(this.params.getMaxMoney()) > 0) + if (valueOut.signum() < 0 || valueOut.compareTo(params.getMaxMoney()) > 0) throw new VerificationException("Transaction output value out of range"); if (isCoinBase) { coinbaseValue = valueOut; } else { - if (valueIn.compareTo(valueOut) < 0 || valueIn.compareTo(this.params.getMaxMoney()) > 0) + if (valueIn.compareTo(valueOut) < 0 || valueIn.compareTo(params.getMaxMoney()) > 0) throw new VerificationException("Transaction input value out of range"); totalFees = totalFees.add(valueIn.subtract(valueOut)); } @@ -314,7 +314,7 @@ public class FullPrunedBlockChain extends AbstractBlockChain { listScriptVerificationResults.add(future); } } - if (totalFees.compareTo(this.params.getMaxMoney()) > 0 || block.getBlockInflation(height).add(totalFees).compareTo(coinbaseValue) < 0) + if (totalFees.compareTo(params.getMaxMoney()) > 0 || block.getBlockInflation(height).add(totalFees).compareTo(coinbaseValue) < 0) throw new VerificationException("Transaction fees out of range"); for (Future future : listScriptVerificationResults) { VerificationException e; @@ -427,12 +427,12 @@ public class FullPrunedBlockChain extends AbstractBlockChain { } // All values were already checked for being non-negative (as it is verified in Transaction.verify()) // but we check again here just for defence in depth. Transactions with zero output value are OK. - if (valueOut.signum() < 0 || valueOut.compareTo(this.params.getMaxMoney()) > 0) + if (valueOut.signum() < 0 || valueOut.compareTo(params.getMaxMoney()) > 0) throw new VerificationException("Transaction output value out of range"); if (isCoinBase) { coinbaseValue = valueOut; } else { - if (valueIn.compareTo(valueOut) < 0 || valueIn.compareTo(this.params.getMaxMoney()) > 0) + if (valueIn.compareTo(valueOut) < 0 || valueIn.compareTo(params.getMaxMoney()) > 0) throw new VerificationException("Transaction input value out of range"); totalFees = totalFees.add(valueIn.subtract(valueOut)); } @@ -444,7 +444,7 @@ public class FullPrunedBlockChain extends AbstractBlockChain { listScriptVerificationResults.add(future); } } - if (totalFees.compareTo(this.params.getMaxMoney()) > 0 || + if (totalFees.compareTo(params.getMaxMoney()) > 0 || newBlock.getHeader().getBlockInflation(newBlock.getHeight()).add(totalFees).compareTo(coinbaseValue) < 0) throw new VerificationException("Transaction fees out of range"); txOutChanges = new TransactionOutputChanges(txOutsCreated, txOutsSpent); diff --git a/core/src/main/java/org/bitcoinj/core/NetworkParameters.java b/core/src/main/java/org/bitcoinj/core/NetworkParameters.java index 2f3bc1ed..54fd387b 100644 --- a/core/src/main/java/org/bitcoinj/core/NetworkParameters.java +++ b/core/src/main/java/org/bitcoinj/core/NetworkParameters.java @@ -18,9 +18,14 @@ package org.bitcoinj.core; import com.google.common.base.Objects; +import org.bitcoinj.core.Block; +import org.bitcoinj.core.StoredBlock; +import org.bitcoinj.core.VerificationException; import org.bitcoinj.net.discovery.*; import org.bitcoinj.params.*; import org.bitcoinj.script.*; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; @@ -245,6 +250,13 @@ public abstract class NetworkParameters implements Serializable { return spendableCoinbaseDepth; } + /** + * Throws an exception if the block's difficulty is not correct. + * + * @throws VerificationException if the block's difficulty is not correct. + */ + public abstract void checkDifficultyTransitions(StoredBlock storedPrev, Block next, final BlockStore blockStore) throws VerificationException, BlockStoreException; + /** * Returns true if the block height is either not a checkpoint, or is a checkpoint and the hash matches. */ diff --git a/core/src/main/java/org/bitcoinj/params/AbstractBitcoinNetParams.java b/core/src/main/java/org/bitcoinj/params/AbstractBitcoinNetParams.java index 45d19f5b..70713b1e 100644 --- a/core/src/main/java/org/bitcoinj/params/AbstractBitcoinNetParams.java +++ b/core/src/main/java/org/bitcoinj/params/AbstractBitcoinNetParams.java @@ -16,12 +16,19 @@ package org.bitcoinj.params; +import java.math.BigInteger; + +import org.bitcoinj.core.Block; import org.bitcoinj.core.Coin; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.StoredBlock; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.Utils; import org.bitcoinj.utils.MonetaryFormat; +import org.bitcoinj.core.VerificationException; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.BlockStoreException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,6 +49,69 @@ public abstract class AbstractBitcoinNetParams extends NetworkParameters { super(); } + @Override + public void checkDifficultyTransitions(final StoredBlock storedPrev, final Block nextBlock, + final BlockStore blockStore) throws VerificationException, BlockStoreException { + Block prev = storedPrev.getHeader(); + + // Is this supposed to be a difficulty transition point? + if ((storedPrev.getHeight() + 1) % this.getInterval() != 0) { + + // No ... so check the difficulty didn't actually change. + if (nextBlock.getDifficultyTarget() != prev.getDifficultyTarget()) + throw new VerificationException("Unexpected change in difficulty at height " + storedPrev.getHeight() + + ": " + Long.toHexString(nextBlock.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 < this.getInterval() - 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()); + } + long elapsed = System.currentTimeMillis() - now; + if (elapsed > 50) + log.info("Difficulty transition traversal took {}msec", elapsed); + + Block blockIntervalAgo = cursor.getHeader(); + int timespan = (int) (prev.getTimeSeconds() - blockIntervalAgo.getTimeSeconds()); + // Limit the adjustment step. + final int targetTimespan = this.getTargetTimespan(); + if (timespan < targetTimespan / 4) + timespan = targetTimespan / 4; + if (timespan > targetTimespan * 4) + timespan = targetTimespan * 4; + + BigInteger newTarget = Utils.decodeCompactBits(prev.getDifficultyTarget()); + newTarget = newTarget.multiply(BigInteger.valueOf(timespan)); + newTarget = newTarget.divide(BigInteger.valueOf(targetTimespan)); + + if (newTarget.compareTo(this.getMaxTarget()) > 0) { + log.info("Difficulty hit proof of work limit: {}", newTarget.toString(16)); + newTarget = this.getMaxTarget(); + } + + int accuracyBytes = (int) (nextBlock.getDifficultyTarget() >>> 24) - 3; + long receivedTargetCompact = nextBlock.getDifficultyTarget(); + + // The calculated difficulty is to a higher precision than received, so reduce here. + BigInteger mask = BigInteger.valueOf(0xFFFFFFL).shiftLeft(accuracyBytes * 8); + newTarget = newTarget.and(mask); + long newTargetCompact = Utils.encodeCompactBits(newTarget); + + if (newTargetCompact != receivedTargetCompact) + throw new VerificationException("Network provided difficulty bits do not match what was calculated: " + + newTargetCompact + " vs " + receivedTargetCompact); + } + @Override public Coin getMaxMoney() { return MAX_MONEY; diff --git a/core/src/main/java/org/bitcoinj/params/TestNet3Params.java b/core/src/main/java/org/bitcoinj/params/TestNet3Params.java index 498c9dfa..4d1a5ecf 100644 --- a/core/src/main/java/org/bitcoinj/params/TestNet3Params.java +++ b/core/src/main/java/org/bitcoinj/params/TestNet3Params.java @@ -17,7 +17,16 @@ package org.bitcoinj.params; +import java.math.BigInteger; +import java.util.Date; + +import org.bitcoinj.core.Block; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.StoredBlock; import org.bitcoinj.core.Utils; +import org.bitcoinj.core.VerificationException; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.BlockStoreException; import static com.google.common.base.Preconditions.checkState; @@ -69,4 +78,39 @@ public class TestNet3Params extends AbstractBitcoinNetParams { public String getPaymentProtocolId() { return PAYMENT_PROTOCOL_ID_TESTNET; } + + // February 16th 2012 + private static final Date testnetDiffDate = new Date(1329264000000L); + + @Override + public void checkDifficultyTransitions(final StoredBlock storedPrev, final Block nextBlock, + final BlockStore blockStore) throws VerificationException, BlockStoreException { + if (nextBlock.getTime().after(testnetDiffDate)) { + Block prev = storedPrev.getHeader(); + + // After 15th February 2012 the rules on the testnet change to avoid people running up the difficulty + // and then leaving, making it too hard to mine a block. On non-difficulty transition points, easy + // blocks are allowed if there has been a span of 20 minutes without one. + final long timeDelta = nextBlock.getTimeSeconds() - prev.getTimeSeconds(); + // There is an integer underflow bug in bitcoin-qt that means mindiff blocks are accepted when time + // goes backwards. + if (timeDelta >= 0 && timeDelta <= NetworkParameters.TARGET_SPACING * 2) { + // Walk backwards until we find a block that doesn't have the easiest proof of work, then check + // that difficulty is equal to that one. + StoredBlock cursor = storedPrev; + while (!cursor.getHeader().equals(getGenesisBlock()) && + cursor.getHeight() % getInterval() != 0 && + cursor.getHeader().getDifficultyTargetAsInteger().equals(getMaxTarget())) + cursor = cursor.getPrev(blockStore); + BigInteger cursorTarget = cursor.getHeader().getDifficultyTargetAsInteger(); + BigInteger newTarget = nextBlock.getDifficultyTargetAsInteger(); + if (!cursorTarget.equals(newTarget)) + throw new VerificationException("Testnet block transition that is not allowed: " + + Long.toHexString(cursor.getHeader().getDifficultyTarget()) + " vs " + + Long.toHexString(nextBlock.getDifficultyTarget())); + } + } else { + super.checkDifficultyTransitions(storedPrev, nextBlock, blockStore); + } + } }