From 6f03669fbd6c368961a25dfd772751d1ca2a1b5b Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Sun, 12 Jul 2015 08:38:32 +0100 Subject: [PATCH] Enforce block version supermajority for BIP 66 onwards. --- .../org/bitcoinj/core/AbstractBlockChain.java | 20 ++- .../main/java/org/bitcoinj/core/Block.java | 30 +++-- .../org/bitcoinj/core/NetworkParameters.java | 33 ++++- .../bitcoinj/core/VerificationException.java | 9 +- .../params/AbstractBitcoinNetParams.java | 2 +- .../org/bitcoinj/params/MainNetParams.java | 8 ++ .../org/bitcoinj/params/RegTestParams.java | 4 + .../org/bitcoinj/params/TestNet2Params.java | 8 ++ .../org/bitcoinj/params/UnitTestParams.java | 8 ++ .../org/bitcoinj/testing/FakeTxBuilder.java | 8 +- .../java/org/bitcoinj/utils/VersionTally.java | 126 ++++++++++++++++++ .../org/bitcoinj/core/BlockChainTest.java | 54 +++++++- .../bitcoinj/core/FullBlockTestGenerator.java | 6 +- .../org/bitcoinj/utils/VersionTallyTest.java | 107 +++++++++++++++ 14 files changed, 395 insertions(+), 28 deletions(-) create mode 100644 core/src/main/java/org/bitcoinj/utils/VersionTally.java create mode 100644 core/src/test/java/org/bitcoinj/utils/VersionTallyTest.java diff --git a/core/src/main/java/org/bitcoinj/core/AbstractBlockChain.java b/core/src/main/java/org/bitcoinj/core/AbstractBlockChain.java index 6082d562..ea1a8e5f 100644 --- a/core/src/main/java/org/bitcoinj/core/AbstractBlockChain.java +++ b/core/src/main/java/org/bitcoinj/core/AbstractBlockChain.java @@ -35,6 +35,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.locks.ReentrantLock; import static com.google.common.base.Preconditions.*; +import org.bitcoinj.utils.VersionTally; /** *

An AbstractBlockChain holds a series of {@link Block} objects, links them together, and knows how to verify that @@ -136,6 +137,8 @@ public abstract class AbstractBlockChain { private double falsePositiveTrend; private double previousFalsePositiveRate; + private final VersionTally versionTally; + /** See {@link #AbstractBlockChain(Context, List, BlockStore)} */ public AbstractBlockChain(NetworkParameters params, List listeners, BlockStore blockStore) throws BlockStoreException { @@ -153,6 +156,8 @@ public abstract class AbstractBlockChain { this.params = context.getParams(); this.listeners = new CopyOnWriteArrayList>(); for (BlockChainListener l : listeners) addListener(l, Threading.SAME_THREAD); + this.versionTally = new VersionTally(context.getParams()); + this.versionTally.initialize(blockStore, chainHead); } /** @@ -458,13 +463,26 @@ public abstract class AbstractBlockChain { } if (expensiveChecks && block.getTimeSeconds() <= getMedianTimestampOfRecentBlocks(head, blockStore)) throw new VerificationException("Block's timestamp is too early"); - + + // BIP 66: Enforce block version 3 once it's a supermajority of blocks + // NOTE: This requires 1,000 blocks since the last checkpoint (on main + // net, less on test) in order to be applied. It is also limited to + // stopping addition of new v2 blocks to the tip of the chain. + if (block.getVersion() == Block.BLOCK_VERSION_BIP34) { + final Integer count = versionTally.getCount(Block.BLOCK_VERSION_BIP66); + if (count != null + && count >= params.getMajorityRejectBlockOutdated()) { + throw new VerificationException.BlockVersionOutOfDate(block.getVersion()); + } + } + // 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); + versionTally.add(block.getVersion()); setChainHead(newStoredBlock); log.debug("Chain is now {} blocks high, running listeners", newStoredBlock.getHeight()); informListenersForNewBlock(block, NewBlockType.BEST_CHAIN, filteredTxHashList, filteredTxn, newStoredBlock); diff --git a/core/src/main/java/org/bitcoinj/core/Block.java b/core/src/main/java/org/bitcoinj/core/Block.java index e0cff3ee..2ab94e1d 100644 --- a/core/src/main/java/org/bitcoinj/core/Block.java +++ b/core/src/main/java/org/bitcoinj/core/Block.java @@ -72,6 +72,12 @@ public class Block extends Message { /** A value for difficultyTarget (nBits) that allows half of all possible hash solutions. Used in unit testing. */ public static final long EASIEST_DIFFICULTY_TARGET = 0x207fFFFFL; + public static final long BLOCK_VERSION_GENESIS = 1; + /** Block version introduced in BIP 34: Height in coinbase */ + public static final long BLOCK_VERSION_BIP34 = 2; + /** Block version introduced in BIP 66: Strict DER signatures */ + public static final long BLOCK_VERSION_BIP66 = 3; + // Fields defined as part of the protocol format. private long version; private Sha256Hash prevBlockHash; @@ -99,10 +105,10 @@ public class Block extends Message { protected int optimalEncodingMessageSize; /** Special case constructor, used for the genesis node, cloneAsHeader and unit tests. */ - Block(NetworkParameters params) { + Block(NetworkParameters params, long setVersion) { super(params); // Set up a few basic things. We are not complete after this though. - version = 1; + version = setVersion; difficultyTarget = 0x1d07fff8L; time = System.currentTimeMillis() / 1000; prevBlockHash = Sha256Hash.ZERO_HASH; @@ -579,7 +585,7 @@ public class Block extends Message { /** Returns a copy of the block, but without any transactions. */ public Block cloneAsHeader() { maybeParseHeader(); - Block block = new Block(params); + Block block = new Block(params, BLOCK_VERSION_GENESIS); copyBitcoinHeaderTo(block); return block; } @@ -1002,17 +1008,17 @@ public class Block extends Message { * Returns a solved block that builds on top of this one. This exists for unit tests. */ @VisibleForTesting - public Block createNextBlock(Address to, long time) { - return createNextBlock(to, null, time, pubkeyForTesting, FIFTY_COINS); + public Block createNextBlock(Address to, long version, long time) { + return createNextBlock(to, version, null, time, pubkeyForTesting, FIFTY_COINS); } /** * Returns a solved block that builds on top of this one. This exists for unit tests. * In this variant you can specify a public key (pubkey) for use in generating coinbase blocks. */ - Block createNextBlock(@Nullable Address to, @Nullable TransactionOutPoint prevOut, long time, - byte[] pubKey, Coin coinbaseValue) { - Block b = new Block(params); + Block createNextBlock(@Nullable Address to, long version, @Nullable TransactionOutPoint prevOut, + long time, byte[] pubKey, Coin coinbaseValue) { + Block b = new Block(params, version); b.setDifficultyTarget(difficultyTarget); b.addCoinbaseTransaction(pubKey, coinbaseValue); @@ -1054,12 +1060,12 @@ public class Block extends Message { @VisibleForTesting public Block createNextBlock(@Nullable Address to, TransactionOutPoint prevOut) { - return createNextBlock(to, prevOut, getTimeSeconds() + 5, pubkeyForTesting, FIFTY_COINS); + return createNextBlock(to, 1, prevOut, getTimeSeconds() + 5, pubkeyForTesting, FIFTY_COINS); } @VisibleForTesting public Block createNextBlock(@Nullable Address to, Coin value) { - return createNextBlock(to, null, getTimeSeconds() + 5, pubkeyForTesting, value); + return createNextBlock(to, 1, null, getTimeSeconds() + 5, pubkeyForTesting, value); } @VisibleForTesting @@ -1069,7 +1075,7 @@ public class Block extends Message { @VisibleForTesting public Block createNextBlockWithCoinbase(byte[] pubKey, Coin coinbaseValue) { - return createNextBlock(null, null, Utils.currentTimeSeconds(), pubKey, coinbaseValue); + return createNextBlock(null, 1, null, Utils.currentTimeSeconds(), pubKey, coinbaseValue); } /** @@ -1078,7 +1084,7 @@ public class Block extends Message { */ @VisibleForTesting Block createNextBlockWithCoinbase(byte[] pubKey) { - return createNextBlock(null, null, Utils.currentTimeSeconds(), pubKey, FIFTY_COINS); + return createNextBlock(null, 1, null, Utils.currentTimeSeconds(), pubKey, FIFTY_COINS); } @VisibleForTesting diff --git a/core/src/main/java/org/bitcoinj/core/NetworkParameters.java b/core/src/main/java/org/bitcoinj/core/NetworkParameters.java index 256bf141..42c9e9f5 100644 --- a/core/src/main/java/org/bitcoinj/core/NetworkParameters.java +++ b/core/src/main/java/org/bitcoinj/core/NetworkParameters.java @@ -87,6 +87,11 @@ public abstract class NetworkParameters { protected int bip32HeaderPub; protected int bip32HeaderPriv; + /** Used to check majorities for block version upgrade */ + protected int majorityEnforceBlockUpgrade; + protected int majorityRejectBlockOutdated; + protected int majorityWindow; + /** * See getId(). This may be null for old deserialized wallets. In that case we derive it heuristically * by looking at the port number. @@ -112,7 +117,7 @@ public abstract class NetworkParameters { } private static Block createGenesis(NetworkParameters n) { - Block genesisBlock = new Block(n); + Block genesisBlock = new Block(n, Block.BLOCK_VERSION_GENESIS); Transaction t = new Transaction(n); try { // A script containing the difficulty bits and the following message: @@ -445,4 +450,30 @@ public abstract class NetworkParameters { * Construct and return a custom serializer. */ public abstract BitcoinSerializer getSerializer(boolean parseLazy, boolean parseRetain); + + /** + * The number of blocks in the last {@link getMajorityWindow()} blocks + * at which to trigger a notice to the user to upgrade their client, where + * the client does not understand those blocks. + */ + public int getMajorityEnforceBlockUpgrade() { + return majorityEnforceBlockUpgrade; + } + + /** + * The number of blocks in the last {@link getMajorityWindow()} blocks + * at which to enforce the requirement that all new blocks are of the + * newer type (i.e. outdated blocks are rejected). + */ + public int getMajorityRejectBlockOutdated() { + return majorityRejectBlockOutdated; + } + + /** + * The sampling window from which the version numbers of blocks are taken + * in order to determine if a new block version is now the majority. + */ + public int getMajorityWindow() { + return majorityWindow; + } } diff --git a/core/src/main/java/org/bitcoinj/core/VerificationException.java b/core/src/main/java/org/bitcoinj/core/VerificationException.java index 5ddf2a87..1891f477 100644 --- a/core/src/main/java/org/bitcoinj/core/VerificationException.java +++ b/core/src/main/java/org/bitcoinj/core/VerificationException.java @@ -42,7 +42,6 @@ public class VerificationException extends RuntimeException { } } - public static class DuplicatedOutPoint extends VerificationException { public DuplicatedOutPoint() { super("Duplicated outpoint"); @@ -68,6 +67,14 @@ public class VerificationException extends RuntimeException { } } + + public static class BlockVersionOutOfDate extends VerificationException { + public BlockVersionOutOfDate(final long version) { + super("Block version #" + + version + " is outdated."); + } + } + public static class UnexpectedCoinbaseInput extends VerificationException { public UnexpectedCoinbaseInput() { super("Coinbase input as input in non-coinbase transaction"); diff --git a/core/src/main/java/org/bitcoinj/params/AbstractBitcoinNetParams.java b/core/src/main/java/org/bitcoinj/params/AbstractBitcoinNetParams.java index 957702d5..8971530a 100644 --- a/core/src/main/java/org/bitcoinj/params/AbstractBitcoinNetParams.java +++ b/core/src/main/java/org/bitcoinj/params/AbstractBitcoinNetParams.java @@ -118,7 +118,7 @@ public abstract class AbstractBitcoinNetParams extends NetworkParameters { if (newTargetCompact != receivedTargetCompact) throw new VerificationException("Network provided difficulty bits do not match what was calculated: " + - newTargetCompact + " vs " + receivedTargetCompact); + Long.toHexString(newTargetCompact) + " vs " + Long.toHexString(receivedTargetCompact)); } @Override diff --git a/core/src/main/java/org/bitcoinj/params/MainNetParams.java b/core/src/main/java/org/bitcoinj/params/MainNetParams.java index c0080c7f..009e7b28 100644 --- a/core/src/main/java/org/bitcoinj/params/MainNetParams.java +++ b/core/src/main/java/org/bitcoinj/params/MainNetParams.java @@ -28,6 +28,10 @@ import static com.google.common.base.Preconditions.*; * Parameters for the main production network on which people trade goods and services. */ public class MainNetParams extends AbstractBitcoinNetParams { + public static final int MAINNET_MAJORITY_WINDOW = 1000; + public static final int MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED = 950; + public static final int MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE = 750; + public MainNetParams() { super(); interval = INTERVAL; @@ -42,6 +46,10 @@ public class MainNetParams extends AbstractBitcoinNetParams { bip32HeaderPub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub". bip32HeaderPriv = 0x0488ADE4; //The 4 byte header that serializes in base58 to "xprv" + majorityEnforceBlockUpgrade = MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE; + majorityRejectBlockOutdated = MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED; + majorityWindow = MAINNET_MAJORITY_WINDOW; + genesisBlock.setDifficultyTarget(0x1d00ffffL); genesisBlock.setTime(1231006505L); genesisBlock.setNonce(2083236893); diff --git a/core/src/main/java/org/bitcoinj/params/RegTestParams.java b/core/src/main/java/org/bitcoinj/params/RegTestParams.java index c77cf723..d426b0af 100644 --- a/core/src/main/java/org/bitcoinj/params/RegTestParams.java +++ b/core/src/main/java/org/bitcoinj/params/RegTestParams.java @@ -35,6 +35,10 @@ public class RegTestParams extends TestNet2Params { subsidyDecreaseBlockCount = 150; port = 18444; id = ID_REGTEST; + + majorityEnforceBlockUpgrade = MainNetParams.MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE; + majorityRejectBlockOutdated = MainNetParams.MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED; + majorityWindow = MainNetParams.MAINNET_MAJORITY_WINDOW; } @Override diff --git a/core/src/main/java/org/bitcoinj/params/TestNet2Params.java b/core/src/main/java/org/bitcoinj/params/TestNet2Params.java index 7b4b0855..d2e01589 100644 --- a/core/src/main/java/org/bitcoinj/params/TestNet2Params.java +++ b/core/src/main/java/org/bitcoinj/params/TestNet2Params.java @@ -25,6 +25,10 @@ import static com.google.common.base.Preconditions.checkState; * based on it. */ public class TestNet2Params extends AbstractBitcoinNetParams { + public static final int TESTNET_MAJORITY_WINDOW = 100; + public static final int TESTNET_MAJORITY_REJECT_BLOCK_OUTDATED = 75; + public static final int TESTNET_MAJORITY_ENFORCE_BLOCK_UPGRADE = 51; + public TestNet2Params() { super(); id = ID_TESTNET; @@ -48,6 +52,10 @@ public class TestNet2Params extends AbstractBitcoinNetParams { addrSeeds = null; bip32HeaderPub = 0x043587CF; bip32HeaderPriv = 0x04358394; + + majorityEnforceBlockUpgrade = TESTNET_MAJORITY_ENFORCE_BLOCK_UPGRADE; + majorityRejectBlockOutdated = TESTNET_MAJORITY_REJECT_BLOCK_OUTDATED; + majorityWindow = TESTNET_MAJORITY_WINDOW; } private static TestNet2Params instance; diff --git a/core/src/main/java/org/bitcoinj/params/UnitTestParams.java b/core/src/main/java/org/bitcoinj/params/UnitTestParams.java index 9ff393e9..f98ab035 100644 --- a/core/src/main/java/org/bitcoinj/params/UnitTestParams.java +++ b/core/src/main/java/org/bitcoinj/params/UnitTestParams.java @@ -25,6 +25,10 @@ import java.math.BigInteger; * {@link org.bitcoinj.core.Block#solve()} by setting difficulty to the easiest possible. */ public class UnitTestParams extends AbstractBitcoinNetParams { + public static final int UNITNET_MAJORITY_WINDOW = 8; + public static final int TESTNET_MAJORITY_REJECT_BLOCK_OUTDATED = 6; + public static final int TESTNET_MAJORITY_ENFORCE_BLOCK_UPGRADE = 4; + // A simple static key/address for re-use in unit tests, to speed things up. public static ECKey TEST_KEY = new ECKey(); public static Address TEST_ADDRESS; @@ -50,6 +54,10 @@ public class UnitTestParams extends AbstractBitcoinNetParams { addrSeeds = null; bip32HeaderPub = 0x043587CF; bip32HeaderPriv = 0x04358394; + + majorityEnforceBlockUpgrade = 3; + majorityRejectBlockOutdated = 4; + majorityWindow = 7; } private static UnitTestParams instance; diff --git a/core/src/main/java/org/bitcoinj/testing/FakeTxBuilder.java b/core/src/main/java/org/bitcoinj/testing/FakeTxBuilder.java index 0ce3a9a7..14ddf3ab 100644 --- a/core/src/main/java/org/bitcoinj/testing/FakeTxBuilder.java +++ b/core/src/main/java/org/bitcoinj/testing/FakeTxBuilder.java @@ -167,11 +167,12 @@ public class FakeTxBuilder { } /** Emulates receiving a valid block that builds on top of the chain. */ - public static BlockPair createFakeBlock(BlockStore blockStore, long timeSeconds, Transaction... transactions) { + public static BlockPair createFakeBlock(BlockStore blockStore, long version, long timeSeconds, + Transaction... transactions) { try { Block chainHead = blockStore.getChainHead().getHeader(); Address to = new ECKey().toAddress(chainHead.getParams()); - Block b = chainHead.createNextBlock(to, timeSeconds); + Block b = chainHead.createNextBlock(to, version, timeSeconds); // Coinbase tx was already added. for (Transaction tx : transactions) { tx.getConfidence().setSource(TransactionConfidence.Source.NETWORK); @@ -191,8 +192,9 @@ public class FakeTxBuilder { } } + /** Emulates receiving a valid block that builds on top of the chain. */ public static BlockPair createFakeBlock(BlockStore blockStore, Transaction... transactions) { - return createFakeBlock(blockStore, Utils.currentTimeSeconds(), transactions); + return createFakeBlock(blockStore, Block.BLOCK_VERSION_GENESIS, Utils.currentTimeSeconds(), transactions); } public static Block makeSolvedTestBlock(BlockStore blockStore, Address coinsTo) throws BlockStoreException { diff --git a/core/src/main/java/org/bitcoinj/utils/VersionTally.java b/core/src/main/java/org/bitcoinj/utils/VersionTally.java new file mode 100644 index 00000000..153b8009 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/utils/VersionTally.java @@ -0,0 +1,126 @@ +/* + * Copyright 2015 Ross Nicoll. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bitcoinj.utils; + +import java.util.Stack; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.StoredBlock; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.BlockStoreException; + +/** + * Caching counter for the block versions within a moving window. This class + * is NOT thread safe (as if two threads are trying to use it concurrently, + * there's risk of getting versions out of sequence). + * + * @see org.bitcoinj.core.NetworkParameters#getMajorityWindow() + * @see org.bitcoinj.core.NetworkParameters#getMajorityEnforceBlockUpgrade() + * @see org.bitcoinj.core.NetworkParameters#getMajorityRejectBlockOutdated() + */ +public class VersionTally { + /** + * Cache of version numbers. + */ + private final long[] versionWindow; + + /** + * Offset within the version window at which the next version will be + * written. + */ + private int versionWriteHead = 0; + + /** + * Number of versions written into the tally. Until this matches the length + * of the version window, we do not have sufficient data to return values. + */ + private int versionsStored = 0; + + public VersionTally(final NetworkParameters params) { + versionWindow = new long[params.getMajorityWindow()]; + } + + /** + * Add a new block version to the tally, and return the count for that version + * within the window. + * + * @param version the block version to add. + */ + public void add(final long version) { + versionWindow[versionWriteHead++] = version; + if (versionWriteHead == versionWindow.length) { + versionWriteHead = 0; + } + versionsStored++; + } + + /** + * Get the count for a block version within the window. + * + * @param version the block version to query. + * @return the count for the block version, or null if the window is not yet + * full. + */ + public Integer getCount(final long version) { + if (versionsStored < versionWindow.length) { + return null; + } + int count = 0; + for (int versionIdx = 0; versionIdx < versionWindow.length; versionIdx++) { + if (versionWindow[versionIdx] == version) { + count++; + } + } + + return count; + } + + /** + * Initialize the version tally from the block store. Note this does not + * search backwards past the start of the block store, so if starting from + * a checkpoint this may not fill the window. + * + * @param blockStore block store to load blocks from. + * @param chainHead current chain tip. + */ + public void initialize(final BlockStore blockStore, final StoredBlock chainHead) + throws BlockStoreException { + StoredBlock versionBlock = chainHead; + final Stack versions = new Stack(); + + // We don't know how many blocks back we can go, so load what we can first + versions.push(versionBlock.getHeader().getVersion()); + for (int headOffset = 0; headOffset < versionWindow.length; headOffset++) { + versionBlock = versionBlock.getPrev(blockStore); + if (null == versionBlock) { + break; + } + versions.push(versionBlock.getHeader().getVersion()); + } + + // Replay the versions into the tally + while (!versions.isEmpty()) { + add(versions.pop()); + } + } + + /** + * Get the size of the version window. + */ + public int size() { + return versionWindow.length; + } +} diff --git a/core/src/test/java/org/bitcoinj/core/BlockChainTest.java b/core/src/test/java/org/bitcoinj/core/BlockChainTest.java index c673c110..3d9f1208 100644 --- a/core/src/test/java/org/bitcoinj/core/BlockChainTest.java +++ b/core/src/test/java/org/bitcoinj/core/BlockChainTest.java @@ -26,8 +26,10 @@ import org.bitcoinj.store.MemoryBlockStore; import org.bitcoinj.testing.FakeTxBuilder; import org.bitcoinj.utils.BriefLogFormatter; import com.google.common.util.concurrent.ListenableFuture; +import org.junit.rules.ExpectedException; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import java.math.BigInteger; @@ -43,6 +45,9 @@ import static org.junit.Assert.*; // Handling of chain splits/reorgs are in ChainSplitTests. public class BlockChainTest { + @Rule + public ExpectedException thrown = ExpectedException.none(); + private BlockChain testNetChain; private Wallet wallet; @@ -157,7 +162,7 @@ public class BlockChainTest { Block prev = unitTestParams.getGenesisBlock(); Utils.setMockClock(System.currentTimeMillis()/1000); for (int i = 0; i < unitTestParams.getInterval() - 1; i++) { - Block newBlock = prev.createNextBlock(coinbaseTo, Utils.currentTimeSeconds()); + Block newBlock = prev.createNextBlock(coinbaseTo, 1, Utils.currentTimeSeconds()); assertTrue(chain.add(newBlock)); prev = newBlock; // The fake chain should seem to be "fast" for the purposes of difficulty calculations. @@ -165,13 +170,13 @@ public class BlockChainTest { } // Now add another block that has no difficulty adjustment, it should be rejected. try { - chain.add(prev.createNextBlock(coinbaseTo, Utils.currentTimeSeconds())); + chain.add(prev.createNextBlock(coinbaseTo, 1, Utils.currentTimeSeconds())); fail(); } catch (VerificationException e) { } // Create a new block with the right difficulty target given our blistering speed relative to the huge amount // of time it's supposed to take (set in the unit test network parameters). - Block b = prev.createNextBlock(coinbaseTo, Utils.currentTimeSeconds()); + Block b = prev.createNextBlock(coinbaseTo, 1, Utils.currentTimeSeconds()); b.setDifficultyTarget(0x201fFFFFL); b.solve(); assertTrue(chain.add(b)); @@ -183,7 +188,7 @@ public class BlockChainTest { assertTrue(testNetChain.add(getBlock1())); Block b2 = getBlock2(); assertTrue(testNetChain.add(b2)); - Block bad = new Block(testNet); + Block bad = new Block(testNet, Block.BLOCK_VERSION_GENESIS); // Merkle root can be anything here, doesn't matter. bad.setMerkleRoot(Sha256Hash.wrap("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); // Nonce was just some number that made the hash < difficulty limit set below, it can be anything. @@ -218,6 +223,43 @@ public class BlockChainTest { // TODO: Test difficulty change is not out of range when a transition period becomes valid. } + /** + * Test that version 2 blocks are rejected once version 3 blocks are a super + * majority. + */ + @Test + public void badBip66Version() throws Exception { + final BlockStore versionBlockStore = new MemoryBlockStore(unitTestParams); + final BlockChain versionChain = new BlockChain(unitTestParams, versionBlockStore); + + // Build a historical chain of version 3 blocks + long timeSeconds = 1231006505; + int blockCount = 0; + FakeTxBuilder.BlockPair chainHead = null; + + // Put in just enough v2 blocks to be a minority + for (blockCount = 0; blockCount < (unitTestParams.getMajorityWindow() - unitTestParams.getMajorityRejectBlockOutdated()); blockCount++) { + chainHead = FakeTxBuilder.createFakeBlock(versionBlockStore, Block.BLOCK_VERSION_BIP34, timeSeconds); + versionChain.add(chainHead.block); + timeSeconds += 60; + } + // Fill the rest of the window with v3 blocks + for (; blockCount < unitTestParams.getMajorityWindow(); blockCount++) { + chainHead = FakeTxBuilder.createFakeBlock(versionBlockStore, Block.BLOCK_VERSION_BIP66, timeSeconds); + versionChain.add(chainHead.block); + timeSeconds += 60; + } + + chainHead = FakeTxBuilder.createFakeBlock(versionBlockStore, Block.BLOCK_VERSION_BIP34, timeSeconds); + // Trying to add a new v2 block should result in rejection + thrown.expect(VerificationException.BlockVersionOutOfDate.class); + try { + versionChain.add(chainHead.block); + } catch(final VerificationException ex) { + throw (Exception) ex.getCause(); + } + } + @Test public void duplicates() throws Exception { // Adding a block twice should not have any effect, in particular it should not send the block to the wallet. @@ -343,7 +385,7 @@ public class BlockChainTest { // Some blocks from the test net. private static Block getBlock2() throws Exception { - Block b2 = new Block(testNet); + Block b2 = new Block(testNet, Block.BLOCK_VERSION_GENESIS); b2.setMerkleRoot(Sha256Hash.wrap("addc858a17e21e68350f968ccd384d6439b64aafa6c193c8b9dd66320470838b")); b2.setNonce(2642058077L); b2.setTime(1296734343L); @@ -354,7 +396,7 @@ public class BlockChainTest { } private static Block getBlock1() throws Exception { - Block b1 = new Block(testNet); + Block b1 = new Block(testNet, Block.BLOCK_VERSION_GENESIS); b1.setMerkleRoot(Sha256Hash.wrap("0e8e58ecdacaa7b3c6304a35ae4ffff964816d2b80b62b58558866ce4e648c10")); b1.setNonce(236038445); b1.setTime(1296734340); diff --git a/core/src/test/java/org/bitcoinj/core/FullBlockTestGenerator.java b/core/src/test/java/org/bitcoinj/core/FullBlockTestGenerator.java index eb598a93..007149ce 100644 --- a/core/src/test/java/org/bitcoinj/core/FullBlockTestGenerator.java +++ b/core/src/test/java/org/bitcoinj/core/FullBlockTestGenerator.java @@ -890,7 +890,7 @@ public class FullBlockTestGenerator { TransactionOutPointWithValue out14 = spendableOutputs.poll(); // A valid block created exactly like b44 to make sure the creation itself works - Block b44 = new Block(params); + Block b44 = new Block(params, Block.BLOCK_VERSION_GENESIS); byte[] outScriptBytes = ScriptBuilder.createOutputScript(ECKey.fromPublicOnly(coinbaseOutKeyPubKey)).getProgram(); { b44.setDifficultyTarget(b43.block.getDifficultyTarget()); @@ -914,7 +914,7 @@ public class FullBlockTestGenerator { TransactionOutPointWithValue out15 = spendableOutputs.poll(); // A block with a non-coinbase as the first tx - Block b45 = new Block(params); + Block b45 = new Block(params, Block.BLOCK_VERSION_GENESIS); { b45.setDifficultyTarget(b44.getDifficultyTarget()); //b45.addCoinbaseTransaction(pubKey, coinbaseValue); @@ -940,7 +940,7 @@ public class FullBlockTestGenerator { blocks.add(new BlockAndValidity(b45, false, true, b44.getHash(), chainHeadHeight + 15, "b45")); // A block with no txn - Block b46 = new Block(params); + Block b46 = new Block(params, Block.BLOCK_VERSION_GENESIS); { b46.transactions = new ArrayList(); b46.setDifficultyTarget(b44.getDifficultyTarget()); diff --git a/core/src/test/java/org/bitcoinj/utils/VersionTallyTest.java b/core/src/test/java/org/bitcoinj/utils/VersionTallyTest.java new file mode 100644 index 00000000..1aeea05a --- /dev/null +++ b/core/src/test/java/org/bitcoinj/utils/VersionTallyTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2015 Ross Nicoll. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bitcoinj.utils; + +import org.bitcoinj.core.BlockChain; +import org.bitcoinj.core.Context; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.StoredBlock; +import org.bitcoinj.params.UnitTestParams; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.BlockStoreException; +import org.bitcoinj.store.MemoryBlockStore; +import org.bitcoinj.testing.FakeTxBuilder; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Before; + +public class VersionTallyTest { + private NetworkParameters unitTestParams; + + public VersionTallyTest() { + } + + @Before + public void setUp() throws Exception { + BriefLogFormatter.initVerbose(); + + unitTestParams = UnitTestParams.get(); + Context context = new Context(unitTestParams); + } + + /** + * Verify that the version tally returns null until it collects enough data. + */ + @Test + public void testNullWhileEmpty() { + VersionTally instance = new VersionTally(unitTestParams); + for (int i = 0; i < unitTestParams.getMajorityWindow(); i++) { + assertNull(instance.getCount(1)); + instance.add(1); + } + assertEquals(unitTestParams.getMajorityWindow(), instance.getCount(1).intValue()); + } + + /** + * Verify that the size of the version tally matches the network parameters. + */ + @Test + public void testSize() { + VersionTally instance = new VersionTally(unitTestParams); + assertEquals(unitTestParams.getMajorityWindow(), instance.size()); + } + + /** + * Verify that version count and substitution works correctly. + */ + @Test + public void testVersionCounts() { + VersionTally instance = new VersionTally(unitTestParams); + + // Fill the tally with 1s + for (int i = 0; i < unitTestParams.getMajorityWindow(); i++) { + assertNull(instance.getCount(1)); + instance.add(1); + } + assertEquals(unitTestParams.getMajorityWindow(), instance.getCount(1).intValue()); + + for (int i = 0; i < unitTestParams.getMajorityWindow(); i++) { + assertEquals(unitTestParams.getMajorityWindow() - i, instance.getCount(1).intValue()); + assertEquals(i, instance.getCount(2).intValue()); + instance.add(2); + } + } + + @Test + public void testInitialize() throws BlockStoreException { + final BlockStore blockStore = new MemoryBlockStore(unitTestParams); + final BlockChain chain = new BlockChain(unitTestParams, blockStore); + + // Build a historical chain of version 2 blocks + long timeSeconds = 1231006505; + StoredBlock chainHead = null; + for (int blockCount = 0; blockCount < unitTestParams.getMajorityWindow(); blockCount++) { + chainHead = FakeTxBuilder.createFakeBlock(blockStore, 2, timeSeconds).storedBlock; + assertEquals(2, chainHead.getHeader().getVersion()); + timeSeconds += 60; + } + + VersionTally instance = new VersionTally(unitTestParams); + instance.initialize(blockStore, chainHead); + assertEquals(unitTestParams.getMajorityWindow(), instance.getCount(2).intValue()); + } +}