/* * Copyright 2012 Matt Corallo. * * 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 com.google.devcoin.core; import com.google.devcoin.params.MainNetParams; import com.google.devcoin.store.BlockStoreException; import com.google.devcoin.store.FullPrunedBlockStore; import com.google.devcoin.store.H2FullPrunedBlockStore; import com.google.devcoin.utils.BlockFileLoader; import com.google.devcoin.utils.BriefLogFormatter; import com.google.devcoin.utils.Threading; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.net.InetAddress; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; /** * A tool for comparing the blocks which are accepted/rejected by bitcoind/bitcoinj * It is designed to run as a testnet-in-a-box network between a single bitcoind node and bitcoinj * It is not an automated unit-test because it requires a bit more set-up...read comments below */ public class BitcoindComparisonTool { private static final Logger log = LoggerFactory.getLogger(BitcoindComparisonTool.class); private static NetworkParameters params; private static FullPrunedBlockStore store; private static FullPrunedBlockChain chain; private static PeerGroup peers; private static Sha256Hash bitcoindChainHead; private static volatile Peer bitcoind; private static volatile InventoryMessage mostRecentInv = null; public static void main(String[] args) throws Exception { BriefLogFormatter.init(); System.out.println("USAGE: bitcoinjBlockStoreLocation runLargeReorgs(1/0) [port=18444]"); boolean runLargeReorgs = args.length > 1 && Integer.parseInt(args[1]) == 1; params = MainNetParams.get(); File blockFile = File.createTempFile("testBlocks", ".dat"); blockFile.deleteOnExit(); FullBlockTestGenerator generator = new FullBlockTestGenerator(params); RuleList blockList = generator.getBlocksToTest(false, runLargeReorgs, blockFile); Iterator<Block> blocks = new BlockFileLoader(params, Arrays.asList(blockFile)); try { store = new H2FullPrunedBlockStore(params, args.length > 0 ? args[0] : "BitcoindComparisonTool", blockList.maximumReorgBlockCount); ((H2FullPrunedBlockStore)store).resetStore(); //store = new MemoryFullPrunedBlockStore(params, blockList.maximumReorgBlockCount); chain = new FullPrunedBlockChain(params, store); } catch (BlockStoreException e) { e.printStackTrace(); System.exit(1); } peers = new PeerGroup(params, chain); peers.setUserAgent("BlockAcceptanceComparisonTool", "1.0"); // bitcoind MUST be on localhost or we will get banned as a DoSer peers.addAddress(new PeerAddress(InetAddress.getByName("localhost"), args.length > 2 ? Integer.parseInt(args[2]) : params.getPort())); final Set<Sha256Hash> blocksRequested = Collections.synchronizedSet(new HashSet<Sha256Hash>()); final AtomicInteger unexpectedInvs = new AtomicInteger(0); peers.addEventListener(new AbstractPeerEventListener() { @Override public void onPeerConnected(Peer peer, int peerCount) { super.onPeerConnected(peer, peerCount); log.info("bitcoind connected"); bitcoind = peer; } @Override public void onPeerDisconnected(Peer peer, int peerCount) { super.onPeerDisconnected(peer, peerCount); log.error("bitcoind node disconnected!"); System.exit(1); } @Override public Message onPreMessageReceived(Peer peer, Message m) { if (m instanceof HeadersMessage) { for (Block block : ((HeadersMessage) m).getBlockHeaders()) bitcoindChainHead = block.getHash(); return null; } else if (m instanceof Block) { log.error("bitcoind sent us a block it already had, make sure bitcoind has no blocks!"); System.exit(1); } else if (m instanceof GetDataMessage) { for (InventoryItem item : ((GetDataMessage)m).items) if (item.type == InventoryItem.Type.Block) blocksRequested.add(item.hash); return null; } else if (m instanceof InventoryMessage) { if (mostRecentInv != null) { log.error("Got an inv when we weren't expecting one"); unexpectedInvs.incrementAndGet(); } mostRecentInv = (InventoryMessage) m; } return m; } }, Threading.SAME_THREAD); peers.addPeerFilterProvider(new PeerFilterProvider() { @Override public long getEarliestKeyCreationTime() { return Long.MAX_VALUE; } @Override public int getBloomFilterElementCount() { return 1; } @Override public BloomFilter getBloomFilter(int size, double falsePositiveRate, long nTweak) { BloomFilter filter = new BloomFilter(1, 0.99, 0); filter.setMatchAll(); return filter; } }); bitcoindChainHead = params.getGenesisBlock().getHash(); // Connect to bitcoind and make sure it has no blocks peers.start(); peers.setMaxConnections(1); peers.downloadBlockChain(); while (bitcoind == null) Thread.sleep(50); ArrayList<Sha256Hash> locator = new ArrayList<Sha256Hash>(1); locator.add(params.getGenesisBlock().getHash()); Sha256Hash hashTo = new Sha256Hash("0000000000000000000000000000000000000000000000000000000000000000"); int differingBlocks = 0; int invalidBlocks = 0; int mempoolRulesFailed = 0; for (Rule rule : blockList.list) { if (rule instanceof BlockAndValidity) { BlockAndValidity block = (BlockAndValidity) rule; boolean threw = false; Block nextBlock = blocks.next(); try { if (chain.add(nextBlock) != block.connects) { log.error("Block didn't match connects flag on block \"" + block.ruleName + "\""); invalidBlocks++; } } catch (VerificationException e) { threw = true; if (!block.throwsException) { log.error("Block didn't match throws flag on block \"" + block.ruleName + "\""); e.printStackTrace(); invalidBlocks++; } else if (block.connects) { log.error("Block didn't match connects flag on block \"" + block.ruleName + "\""); e.printStackTrace(); invalidBlocks++; } } if (!threw && block.throwsException) { log.error("Block didn't match throws flag on block \"" + block.ruleName + "\""); invalidBlocks++; } else if (!chain.getChainHead().getHeader().getHash().equals(block.hashChainTipAfterBlock)) { log.error("New block head didn't match the correct value after block \"" + block.ruleName + "\""); invalidBlocks++; } else if (chain.getChainHead().getHeight() != block.heightAfterBlock) { log.error("New block head didn't match the correct height after block " + block.ruleName); invalidBlocks++; } InventoryMessage message = new InventoryMessage(params); message.addBlock(nextBlock); bitcoind.sendMessage(message); // bitcoind doesn't request blocks inline so we can't rely on a ping for synchronization for (int i = 0; !blocksRequested.contains(nextBlock.getHash()); i++) { if (i % 20 == 19) log.error("bitcoind still hasn't requested block " + block.ruleName + " with hash " + nextBlock.getHash()); Thread.sleep(50); } bitcoind.sendMessage(nextBlock); locator.clear(); locator.add(bitcoindChainHead); bitcoind.sendMessage(new GetHeadersMessage(params, locator, hashTo)); bitcoind.ping().get(); if (!chain.getChainHead().getHeader().getHash().equals(bitcoindChainHead)) { differingBlocks++; log.error("bitcoind and bitcoinj acceptance differs on block \"" + block.ruleName + "\""); } log.info("Block \"" + block.ruleName + "\" completed processing"); } else if (rule instanceof MemoryPoolState) { MemoryPoolMessage message = new MemoryPoolMessage(); bitcoind.sendMessage(message); bitcoind.ping().get(); if (mostRecentInv == null && !((MemoryPoolState) rule).mempool.isEmpty()) { log.error("bitcoind had an empty mempool, but we expected some transactions on rule " + rule.ruleName); mempoolRulesFailed++; } else if (mostRecentInv != null && ((MemoryPoolState) rule).mempool.isEmpty()) { log.error("bitcoind had a non-empty mempool, but we expected an empty one on rule " + rule.ruleName); mempoolRulesFailed++; } else if (mostRecentInv != null) { Set<InventoryItem> originalRuleSet = new HashSet<InventoryItem>(((MemoryPoolState)rule).mempool); boolean matches = mostRecentInv.items.size() == ((MemoryPoolState)rule).mempool.size(); for (InventoryItem item : mostRecentInv.items) if (!((MemoryPoolState) rule).mempool.remove(item)) matches = false; if (matches) continue; log.error("bitcoind's mempool didn't match what we were expecting on rule " + rule.ruleName); log.info(" bitcoind's mempool was: "); for (InventoryItem item : mostRecentInv.items) log.info(" " + item.hash); log.info(" The expected mempool was: "); for (InventoryItem item : originalRuleSet) log.info(" " + item.hash); mempoolRulesFailed++; } mostRecentInv = null; } else { log.error("Unknown rule"); } } log.info("Done testing.\n" + "Blocks which were not handled the same between bitcoind/bitcoinj: " + differingBlocks + "\n" + "Blocks which should/should not have been accepted but weren't/were: " + invalidBlocks + "\n" + "Transactions which were/weren't in memory pool but shouldn't/should have been: " + mempoolRulesFailed + "\n" + "Unexpected inv messages: " + unexpectedInvs.get()); System.exit(differingBlocks > 0 || invalidBlocks > 0 || mempoolRulesFailed > 0 || unexpectedInvs.get() > 0 ? 1 : 0); } }