/*
 * Copyright by the original author or authors.
 * 
 * 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.core;

import org.bitcoinj.base.Coin;
import org.bitcoinj.base.Sha256Hash;
import org.bitcoinj.base.VarInt;
import org.bitcoinj.base.internal.TimeUtils;
import org.bitcoinj.base.internal.ByteUtils;
import org.bitcoinj.core.Transaction.SigHash;
import org.bitcoinj.crypto.ECKey;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.crypto.internal.CryptoUtils;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptException;
import org.bitcoinj.script.ScriptPattern;

import javax.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.time.Period;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Random;
import java.util.Set;

import static org.bitcoinj.base.Coin.FIFTY_COINS;
import static org.bitcoinj.base.Coin.SATOSHI;
import static org.bitcoinj.base.Coin.ZERO;
import static org.bitcoinj.base.internal.Preconditions.checkArgument;
import static org.bitcoinj.base.internal.Preconditions.checkState;
import static org.bitcoinj.script.ScriptOpCodes.OP_1;
import static org.bitcoinj.script.ScriptOpCodes.OP_2DUP;
import static org.bitcoinj.script.ScriptOpCodes.OP_CHECKMULTISIG;
import static org.bitcoinj.script.ScriptOpCodes.OP_CHECKMULTISIGVERIFY;
import static org.bitcoinj.script.ScriptOpCodes.OP_CHECKSIG;
import static org.bitcoinj.script.ScriptOpCodes.OP_CHECKSIGVERIFY;
import static org.bitcoinj.script.ScriptOpCodes.OP_ELSE;
import static org.bitcoinj.script.ScriptOpCodes.OP_ENDIF;
import static org.bitcoinj.script.ScriptOpCodes.OP_EQUAL;
import static org.bitcoinj.script.ScriptOpCodes.OP_FALSE;
import static org.bitcoinj.script.ScriptOpCodes.OP_HASH160;
import static org.bitcoinj.script.ScriptOpCodes.OP_IF;
import static org.bitcoinj.script.ScriptOpCodes.OP_INVALIDOPCODE;
import static org.bitcoinj.script.ScriptOpCodes.OP_NOP;
import static org.bitcoinj.script.ScriptOpCodes.OP_PUSHDATA1;
import static org.bitcoinj.script.ScriptOpCodes.OP_PUSHDATA4;
import static org.bitcoinj.script.ScriptOpCodes.OP_RETURN;
import static org.bitcoinj.script.ScriptOpCodes.OP_TRUE;

/**
 * YOU ARE READING THIS CODE BECAUSE EITHER...
 *
 * a) You are testing an alternative implementation with full validation rules. If you are doing this, you should go
 *    rethink your life. Seriously, why are you reimplementing Bitcoin consensus rules? Instead, go work on making
 *    Bitcoin Core consensus rules a shared library and use that. Seriously, you wont get it right, and starting with
 *    this tester as a way to try to do so will simply end in pain and lost coins. SERIOUSLY, JUST STOP!
 *
 * b) Bitcoin Core is failing some test in here and you're wondering what test is causing failure. Just stop. There is no
 *    hope trying to read this file and decipher it. Give up and ping BlueMatt. Seriously, this stuff is a huge mess.
 *
 * c) You are trying to add a new test. STOP! WHY THE HELL WOULD YOU EVEN DO THAT? GO REWRITE THIS TESTER!
 *
 * d) You are BlueMatt and you're trying to hack more crap onto this multi-headed lopsided Proof Of Stake. Why are you
 *    doing this? Seriously, why have you not rewritten this thing yet? WTF man...
 *
 * IN ANY CASE, STOP READING NOW. IT WILL SAVE YOU MUCH PAIN AND MISERY LATER
 */

class NewBlock {
    public Block block;
    private TransactionOutPointWithValue spendableOutput;
    public NewBlock(Block block, TransactionOutPointWithValue spendableOutput) {
        this.block = block; this.spendableOutput = spendableOutput;
    }
    // Wrappers to make it more block-like
    public Sha256Hash getHash() { return block.getHash(); }
    public void solve() { block.solve(); }
    public void addTransaction(Transaction tx) { block.addTransaction(tx); }

    public TransactionOutPointWithValue getCoinbaseOutput() {
        return new TransactionOutPointWithValue(block.getTransactions().get(0), 0);
    }

    public TransactionOutPointWithValue getSpendableOutput() {
        return spendableOutput;
    }
}

class TransactionOutPointWithValue {
    public TransactionOutPoint outpoint;
    public Coin value;
    public Script scriptPubKey;

    public TransactionOutPointWithValue(TransactionOutPoint outpoint, Coin value, Script scriptPubKey) {
        this.outpoint = outpoint;
        this.value = value;
        this.scriptPubKey = scriptPubKey;
    }

    public TransactionOutPointWithValue(Transaction tx, int outputIndex) {
        this(new TransactionOutPoint(outputIndex, tx.getTxId()),
                tx.getOutput(outputIndex).getValue(), tx.getOutput(outputIndex).getScriptPubKey());
    }
}

/** An arbitrary rule which the testing client must match */
class Rule {
    String ruleName;
    Rule(String ruleName) {
        this.ruleName = ruleName;
    }
}

/**
 * A test which checks the mempool state (ie defined which transactions should be in memory pool
 */
class MemoryPoolState extends Rule {
    Set<InventoryItem> mempool;
    public MemoryPoolState(Set<InventoryItem> mempool, String ruleName) {
        super(ruleName);
        this.mempool = mempool;
    }
}

class RuleList {
    public List<Rule> list;
    public int maximumReorgBlockCount;
    Map<Sha256Hash, Block> hashHeaderMap;
    public RuleList(List<Rule> list, Map<Sha256Hash, Block> hashHeaderMap, int maximumReorgBlockCount) {
        this.list = list;
        this.hashHeaderMap = hashHeaderMap;
        this.maximumReorgBlockCount = maximumReorgBlockCount;
    }
}

public class FullBlockTestGenerator {
    // Used by BitcoindComparisonTool and AbstractFullPrunedBlockChainTest to create test cases
    private NetworkParameters params;
    private ECKey coinbaseOutKey;
    private byte[] coinbaseOutKeyPubKey;

    // Used to double-check that we are always using the right next-height
    private Map<Sha256Hash, Integer> blockToHeightMap = new HashMap<>();

    private Map<Sha256Hash, Block> hashHeaderMap = new HashMap<>();
    private Map<Sha256Hash, Sha256Hash> coinbaseBlockMap = new HashMap<>();

    public FullBlockTestGenerator(NetworkParameters params) {
        this.params = params;
        coinbaseOutKey = new ECKey();
        coinbaseOutKeyPubKey = coinbaseOutKey.getPubKey();
        TimeUtils.setMockClock();
    }

    public RuleList getBlocksToTest(boolean runBarelyExpensiveTests, boolean runExpensiveTests, File blockStorageFile) throws ScriptException, ProtocolException, IOException {
        final FileOutputStream outStream = blockStorageFile != null ? new FileOutputStream(blockStorageFile) : null;

        final Script OP_TRUE_SCRIPT = new ScriptBuilder().op(OP_TRUE).build();
        final Script OP_NOP_SCRIPT = new ScriptBuilder().op(OP_NOP).build();

        // TODO: Rename this variable.
        List<Rule> blocks = new LinkedList<Rule>() {
            @Override
            public boolean add(Rule element) {
                if (outStream != null && element instanceof BlockAndValidity) {
                    try {
                        ByteUtils.writeInt32BE(params.getPacketMagic(), outStream);
                        byte[] block = ((BlockAndValidity) element).block.serialize();
                        byte[] length = new byte[4];
                        ByteUtils.writeInt32LE(block.length, length, 0);
                        outStream.write(length);
                        outStream.write(block);
                        ((BlockAndValidity)element).block = null;
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
                return super.add(element);
            }
        };
        RuleList ret = new RuleList(blocks, hashHeaderMap, 10);

        Queue<TransactionOutPointWithValue> spendableOutputs = new LinkedList<>();

        int chainHeadHeight = 1;
        Block chainHead = params.getGenesisBlock().createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, coinbaseOutKeyPubKey, chainHeadHeight);
        blocks.add(new BlockAndValidity(chainHead, true, false, chainHead.getHash(), 1, "Initial Block"));
        spendableOutputs.offer(new TransactionOutPointWithValue(
                new TransactionOutPoint(0, chainHead.getTransactions().get(0).getTxId()),
                FIFTY_COINS, chainHead.getTransactions().get(0).getOutput(0).getScriptPubKey()));
        for (int i = 1; i < params.getSpendableCoinbaseDepth(); i++) {
            chainHead = chainHead.createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, coinbaseOutKeyPubKey, chainHeadHeight);
            chainHeadHeight++;
            blocks.add(new BlockAndValidity(chainHead, true, false, chainHead.getHash(), i+1, "Initial Block chain output generation"));
            spendableOutputs.offer(new TransactionOutPointWithValue(
                    new TransactionOutPoint(0, chainHead.getTransactions().get(0).getTxId()),
                    FIFTY_COINS, chainHead.getTransactions().get(0).getOutput(0).getScriptPubKey()));
        }

        // Start by building a couple of blocks on top of the genesis block.
        NewBlock b1 = createNextBlock(chainHead, chainHeadHeight + 1, spendableOutputs.poll(), null);
        blocks.add(new BlockAndValidity(b1, true, false, b1.getHash(), chainHeadHeight + 1, "b1"));
        spendableOutputs.offer(b1.getCoinbaseOutput());

        TransactionOutPointWithValue out1 = spendableOutputs.poll(); checkState(out1 != null);
        NewBlock b2 = createNextBlock(b1, chainHeadHeight + 2, out1, null);
        blocks.add(new BlockAndValidity(b2, true, false, b2.getHash(), chainHeadHeight + 2, "b2"));
        // Make sure nothing funky happens if we try to re-add b2
        blocks.add(new BlockAndValidity(b2, true, false, b2.getHash(), chainHeadHeight + 2, "b2"));
        spendableOutputs.offer(b2.getCoinbaseOutput());
        // We now have the following chain (which output is spent is in parentheses):
        //     genesis -> b1 (0) -> b2 (1)
        //
        // so fork like this:
        //
        //     genesis -> b1 (0) -> b2 (1)
        //                      \-> b3 (1)
        //
        // Nothing should happen at this point. We saw b2 first so it takes priority.
        NewBlock b3 = createNextBlock(b1, chainHeadHeight + 2, out1, null);
        blocks.add(new BlockAndValidity(b3, true, false, b2.getHash(), chainHeadHeight + 2, "b3"));
        // Make sure nothing breaks if we add b3 twice
        blocks.add(new BlockAndValidity(b3, true, false, b2.getHash(), chainHeadHeight + 2, "b3"));

        // Now we add another block to make the alternative chain longer.
        //
        //     genesis -> b1 (0) -> b2 (1)
        //                      \-> b3 (1) -> b4 (2)
        //
        TransactionOutPointWithValue out2 = Objects.requireNonNull(spendableOutputs.poll());
        NewBlock b4 = createNextBlock(b3, chainHeadHeight + 3, out2, null);
        blocks.add(new BlockAndValidity(b4, true, false, b4.getHash(), chainHeadHeight + 3, "b4"));

        // ... and back to the first chain.
        NewBlock b5 = createNextBlock(b2, chainHeadHeight + 3, out2, null);
        blocks.add(new BlockAndValidity(b5, true, false, b4.getHash(), chainHeadHeight + 3, "b5"));
        spendableOutputs.offer(b5.getCoinbaseOutput());

        TransactionOutPointWithValue out3 = spendableOutputs.poll();

        NewBlock b6 = createNextBlock(b5, chainHeadHeight + 4, out3, null);
        blocks.add(new BlockAndValidity(b6, true, false, b6.getHash(), chainHeadHeight + 4, "b6"));
        //
        //     genesis -> b1 (0) -> b2 (1) -> b5 (2) -> b6 (3)
        //                      \-> b3 (1) -> b4 (2)
        //

        // Try to create a fork that double-spends
        //     genesis -> b1 (0) -> b2 (1) -> b5 (2) -> b6 (3)
        //                                          \-> b7 (2) -> b8 (4)
        //                      \-> b3 (1) -> b4 (2)
        //
        NewBlock b7 = createNextBlock(b5, chainHeadHeight + 5, out2, null);
        blocks.add(new BlockAndValidity(b7, true, false, b6.getHash(), chainHeadHeight + 4, "b7"));

        TransactionOutPointWithValue out4 = spendableOutputs.poll();

        NewBlock b8 = createNextBlock(b7, chainHeadHeight + 6, out4, null);
        blocks.add(new BlockAndValidity(b8, false, true, b6.getHash(), chainHeadHeight + 4, "b8"));

        // Try to create a block that has too much fee
        //     genesis -> b1 (0) -> b2 (1) -> b5 (2) -> b6 (3)
        //                                                    \-> b9 (4)
        //                      \-> b3 (1) -> b4 (2)
        //
        NewBlock b9 = createNextBlock(b6, chainHeadHeight + 5, out4, SATOSHI);
        blocks.add(new BlockAndValidity(b9, false, true, b6.getHash(), chainHeadHeight + 4, "b9"));

        // Create a fork that ends in a block with too much fee (the one that causes the reorg)
        //     genesis -> b1 (0) -> b2 (1) -> b5 (2) -> b6  (3)
        //                                          \-> b10 (3) -> b11 (4)
        //                      \-> b3 (1) -> b4 (2)
        //
        NewBlock b10 = createNextBlock(b5, chainHeadHeight + 4, out3, null);
        blocks.add(new BlockAndValidity(b10, true, false, b6.getHash(), chainHeadHeight + 4, "b10"));

        NewBlock b11 = createNextBlock(b10, chainHeadHeight + 5, out4, SATOSHI);
        blocks.add(new BlockAndValidity(b11, false, true, b6.getHash(), chainHeadHeight + 4, "b11"));

        // Try again, but with a valid fork first
        //     genesis -> b1 (0) -> b2 (1) -> b5 (2) -> b6  (3)
        //                                          \-> b12 (3) -> b13 (4) -> b14 (5)
        //                                              (b12 added last)
        //                      \-> b3 (1) -> b4 (2)
        //
        NewBlock b12 = createNextBlock(b5, chainHeadHeight + 4, out3, null);
        spendableOutputs.offer(b12.getCoinbaseOutput());

        NewBlock b13 = createNextBlock(b12, chainHeadHeight + 5, out4, null);
        blocks.add(new BlockAndValidity(b13, false, false, b6.getHash(), chainHeadHeight + 4, "b13"));
        // Make sure we don't die if an orphan gets added twice
        blocks.add(new BlockAndValidity(b13, false, false, b6.getHash(), chainHeadHeight + 4, "b13"));
        spendableOutputs.offer(b13.getCoinbaseOutput());

        TransactionOutPointWithValue out5 = spendableOutputs.poll();

        NewBlock b14 = createNextBlock(b13, chainHeadHeight + 6, out5, SATOSHI);
        // This will be "validly" added, though its actually invalid, it will just be marked orphan
        // and will be discarded when an attempt is made to reorg to it.
        // TODO: Use a WeakReference to check that it is freed properly after the reorg
        blocks.add(new BlockAndValidity(b14, false, false, b6.getHash(), chainHeadHeight + 4, "b14"));
        // Make sure we don't die if an orphan gets added twice
        blocks.add(new BlockAndValidity(b14, false, false, b6.getHash(), chainHeadHeight + 4, "b14"));

        blocks.add(new BlockAndValidity(b12, false, true, b13.getHash(), chainHeadHeight + 5, "b12"));

        // Add a block with MAX_BLOCK_SIGOPS and one with one more sigop
        //     genesis -> b1 (0) -> b2 (1) -> b5 (2) -> b6  (3)
        //                                          \-> b12 (3) -> b13 (4) -> b15 (5) -> b16 (6)
        //                      \-> b3 (1) -> b4 (2)
        //
        NewBlock b15 = createNextBlock(b13, chainHeadHeight + 6, out5, null);
        {
            int sigOps = 0;
            for (Transaction tx : b15.block.getTransactions())
                sigOps += tx.getSigOpCount();
            Transaction tx = new Transaction();
            byte[] outputScript = new byte[Block.MAX_BLOCK_SIGOPS - sigOps];
            Arrays.fill(outputScript, (byte) OP_CHECKSIG);
            tx.addOutput(new TransactionOutput(tx, SATOSHI, outputScript));
            addOnlyInputToTransaction(tx, b15);
            b15.addTransaction(tx);

            sigOps = 0;
            for (Transaction tx2 : b15.block.getTransactions())
                sigOps += tx2.getSigOpCount();
            checkState(sigOps == Block.MAX_BLOCK_SIGOPS);
        }
        b15.solve();

        blocks.add(new BlockAndValidity(b15, true, false, b15.getHash(), chainHeadHeight + 6, "b15"));
        spendableOutputs.offer(b15.getCoinbaseOutput());

        TransactionOutPointWithValue out6 = spendableOutputs.poll();

        NewBlock b16 = createNextBlock(b15, chainHeadHeight + 7, out6, null);
        {
            int sigOps = 0;
            for (Transaction tx : b16.block.getTransactions()) {
                sigOps += tx.getSigOpCount();
            }
            Transaction tx = new Transaction();
            byte[] outputScript = new byte[Block.MAX_BLOCK_SIGOPS - sigOps + 1];
            Arrays.fill(outputScript, (byte) OP_CHECKSIG);
            tx.addOutput(new TransactionOutput(tx, SATOSHI, outputScript));
            addOnlyInputToTransaction(tx, b16);
            b16.addTransaction(tx);

            sigOps = 0;
            for (Transaction tx2 : b16.block.getTransactions())
                sigOps += tx2.getSigOpCount();
            checkState(sigOps == Block.MAX_BLOCK_SIGOPS + 1);
        }
        b16.solve();

        blocks.add(new BlockAndValidity(b16, false, true, b15.getHash(), chainHeadHeight + 6, "b16"));

        // Attempt to spend a transaction created on a different fork
        //     genesis -> b1 (0) -> b2 (1) -> b5 (2) -> b6  (3)
        //                                          \-> b12 (3) -> b13 (4) -> b15 (5) -> b17 (6)
        //                      \-> b3 (1) -> b4 (2)
        //
        NewBlock b17 = createNextBlock(b15, chainHeadHeight + 7, out6, null);
        {
            Transaction tx = new Transaction();
            tx.addOutput(new TransactionOutput(tx, SATOSHI, new byte[] {}));
            addOnlyInputToTransaction(tx, b3);
            b17.addTransaction(tx);
        }
        b17.solve();
        blocks.add(new BlockAndValidity(b17, false, true, b15.getHash(), chainHeadHeight + 6, "b17"));

        // Attempt to spend a transaction created on a different fork (on a fork this time)
        //     genesis -> b1 (0) -> b2 (1) -> b5 (2) -> b6  (3)
        //                                          \-> b12 (3) -> b13 (4) -> b15 (5)
        //                                                                \-> b18 (5) -> b19 (6)
        //                      \-> b3 (1) -> b4 (2)
        //
        NewBlock b18 = createNextBlock(b13, chainHeadHeight + 6, out5, null);
        {
            Transaction tx = new Transaction();
            tx.addOutput(new TransactionOutput(tx, SATOSHI, new byte[] {}));
            addOnlyInputToTransaction(tx, b3);
            b18.addTransaction(tx);
        }
        b18.solve();
        blocks.add(new BlockAndValidity(b18, true, false, b15.getHash(), chainHeadHeight + 6, "b17"));

        NewBlock b19 = createNextBlock(b18, chainHeadHeight + 7, out6, null);
        blocks.add(new BlockAndValidity(b19, false, true, b15.getHash(), chainHeadHeight + 6, "b19"));

        // Attempt to spend a coinbase at depth too low
        //     genesis -> b1 (0) -> b2 (1) -> b5 (2) -> b6  (3)
        //                                          \-> b12 (3) -> b13 (4) -> b15 (5) -> b20 (7)
        //                      \-> b3 (1) -> b4 (2)
        //
        TransactionOutPointWithValue out7 = spendableOutputs.poll();

        NewBlock b20 = createNextBlock(b15.block, chainHeadHeight + 7, out7, null);
        blocks.add(new BlockAndValidity(b20, false, true, b15.getHash(), chainHeadHeight + 6, "b20"));

        // Attempt to spend a coinbase at depth too low (on a fork this time)
        //     genesis -> b1 (0) -> b2 (1) -> b5 (2) -> b6  (3)
        //                                          \-> b12 (3) -> b13 (4) -> b15 (5)
        //                                                                \-> b21 (6) -> b22 (5)
        //                      \-> b3 (1) -> b4 (2)
        //
        NewBlock b21 = createNextBlock(b13, chainHeadHeight + 6, out6, null);
        blocks.add(new BlockAndValidity(b21.block, true, false, b15.getHash(), chainHeadHeight + 6, "b21"));
        NewBlock b22 = createNextBlock(b21, chainHeadHeight + 7, out5, null);
        blocks.add(new BlockAndValidity(b22.block, false, true, b15.getHash(), chainHeadHeight + 6, "b22"));

        // Create a block on either side of MAX_BLOCK_SIZE and make sure its accepted/rejected
        //     genesis -> b1 (0) -> b2 (1) -> b5 (2) -> b6  (3)
        //                                          \-> b12 (3) -> b13 (4) -> b15 (5) -> b23 (6)
        //                                                                           \-> b24 (6) -> b25 (7)
        //                      \-> b3 (1) -> b4 (2)
        //
        NewBlock b23 = createNextBlock(b15, chainHeadHeight + 7, out6, null);
        {
            Transaction tx = new Transaction();
            byte[] outputScript = new byte[Block.MAX_BLOCK_SIZE - b23.block.messageSize() - 65];
            Arrays.fill(outputScript, (byte) OP_FALSE);
            tx.addOutput(new TransactionOutput(tx, ZERO, outputScript));
            addOnlyInputToTransaction(tx, b23);
            b23.addTransaction(tx);
        }
        b23.solve();
        checkState(b23.block.messageSize() == Block.MAX_BLOCK_SIZE);
        blocks.add(new BlockAndValidity(b23, true, false, b23.getHash(), chainHeadHeight + 7, "b23"));
        spendableOutputs.offer(b23.getCoinbaseOutput());

        NewBlock b24 = createNextBlock(b15, chainHeadHeight + 7, out6, null);
        {
            Transaction tx = new Transaction();
            byte[] outputScript = new byte[Block.MAX_BLOCK_SIZE - b24.block.messageSize() - 64];
            Arrays.fill(outputScript, (byte) OP_FALSE);
            tx.addOutput(new TransactionOutput(tx, ZERO, outputScript));
            addOnlyInputToTransaction(tx, b24);
            b24.addTransaction(tx);
        }
        b24.solve();
        checkState(b24.block.messageSize() == Block.MAX_BLOCK_SIZE + 1);
        blocks.add(new BlockAndValidity(b24, false, true, b23.getHash(), chainHeadHeight + 7, "b24"));

        // Extend the b24 chain to make sure bitcoind isn't accepting b24
        NewBlock b25 = createNextBlock(b24, chainHeadHeight + 8, out7, null);
        blocks.add(new BlockAndValidity(b25, false, false, b23.getHash(), chainHeadHeight + 7, "b25"));

        // Create blocks with a coinbase input script size out of range
        //     genesis -> b1 (0) -> b2 (1) -> b5 (2) -> b6  (3)
        //                                          \-> b12 (3) -> b13 (4) -> b15 (5) -> b23 (6) -> b30 (7)
        //                                                                           \-> ... (6) -> ... (7)
        //                      \-> b3 (1) -> b4 (2)
        //
        NewBlock b26 = createNextBlock(b15, chainHeadHeight + 7, out6, null);
        // 1 is too small, but we already generate every other block with 2, so that is tested
        b26.block.getTransactions().get(0).getInput(0).clearScriptBytes();
        b26.block.setMerkleRoot(null);
        b26.solve();
        blocks.add(new BlockAndValidity(b26, false, true, b23.getHash(), chainHeadHeight + 7, "b26"));

        // Extend the b26 chain to make sure bitcoind isn't accepting b26
        NewBlock b27 = createNextBlock(b26, chainHeadHeight + 8, out7, null);
        blocks.add(new BlockAndValidity(b27, false, false, b23.getHash(), chainHeadHeight + 7, "b27"));

        NewBlock b28 = createNextBlock(b15, chainHeadHeight + 7, out6, null);
        {
            byte[] coinbase = new byte[101];
            Arrays.fill(coinbase, (byte)0);
            b28.block.getTransactions().get(0).getInput(0).setScriptBytes(coinbase);
        }
        b28.block.setMerkleRoot(null);
        b28.solve();
        blocks.add(new BlockAndValidity(b28, false, true, b23.getHash(), chainHeadHeight + 7, "b28"));

        // Extend the b28 chain to make sure bitcoind isn't accepting b28
        NewBlock b29 = createNextBlock(b28, chainHeadHeight + 8, out7, null);
        blocks.add(new BlockAndValidity(b29, false, false, b23.getHash(), chainHeadHeight + 7, "b29"));

        NewBlock b30 = createNextBlock(b23, chainHeadHeight + 8, out7, null);
        {
            byte[] coinbase = new byte[100];
            Arrays.fill(coinbase, (byte)0);
            b30.block.getTransactions().get(0).getInput(0).setScriptBytes(coinbase);
        }
        b30.block.setMerkleRoot(null);
        b30.solve();
        blocks.add(new BlockAndValidity(b30, true, false, b30.getHash(), chainHeadHeight + 8, "b30"));
        spendableOutputs.offer(b30.getCoinbaseOutput());

        // Check sigops of OP_CHECKMULTISIG/OP_CHECKMULTISIGVERIFY/OP_CHECKSIGVERIFY
        // 6  (3)
        // 12 (3) -> b13 (4) -> b15 (5) -> b23 (6) -> b30 (7) -> b31 (8) -> b33 (9) -> b35 (10)
        //                                                                                     \-> b36 (11)
        //                                                                         \-> b34 (10)
        //                                                              \-> b32 (9)
        //
        TransactionOutPointWithValue out8 = spendableOutputs.poll();

        NewBlock b31 = createNextBlock(b30, chainHeadHeight + 9, out8, null);
        {
            int sigOps = 0;
            for (Transaction tx : b31.block.transactions) {
                sigOps += tx.getSigOpCount();
            }
            Transaction tx = new Transaction();
            byte[] outputScript = new byte[(Block.MAX_BLOCK_SIGOPS - sigOps)/20];
            Arrays.fill(outputScript, (byte) OP_CHECKMULTISIG);
            tx.addOutput(new TransactionOutput(tx, SATOSHI, outputScript));
            addOnlyInputToTransaction(tx, b31);
            b31.addTransaction(tx);
        }
        b31.solve();

        blocks.add(new BlockAndValidity(b31, true, false, b31.getHash(), chainHeadHeight + 9, "b31"));
        spendableOutputs.offer(b31.getCoinbaseOutput());

        TransactionOutPointWithValue out9 = spendableOutputs.poll();

        NewBlock b32 = createNextBlock(b31, chainHeadHeight + 10, out9, null);
        {
            int sigOps = 0;
            for (Transaction tx : b32.block.transactions) {
                sigOps += tx.getSigOpCount();
            }
            Transaction tx = new Transaction();
            byte[] outputScript = new byte[(Block.MAX_BLOCK_SIGOPS - sigOps)/20 + (Block.MAX_BLOCK_SIGOPS - sigOps)%20 + 1];
            Arrays.fill(outputScript, (byte) OP_CHECKMULTISIG);
            for (int i = 0; i < (Block.MAX_BLOCK_SIGOPS - sigOps)%20; i++)
                outputScript[i] = (byte) OP_CHECKSIG;
            tx.addOutput(new TransactionOutput(tx, SATOSHI, outputScript));
            addOnlyInputToTransaction(tx, b32);
            b32.addTransaction(tx);
        }
        b32.solve();
        blocks.add(new BlockAndValidity(b32, false, true, b31.getHash(), chainHeadHeight + 9, "b32"));

        NewBlock b33 = createNextBlock(b31, chainHeadHeight + 10, out9, null);
        {
            int sigOps = 0;
            for (Transaction tx : b33.block.transactions) {
                sigOps += tx.getSigOpCount();
            }
            Transaction tx = new Transaction();
            byte[] outputScript = new byte[(Block.MAX_BLOCK_SIGOPS - sigOps)/20];
            Arrays.fill(outputScript, (byte) OP_CHECKMULTISIGVERIFY);
            tx.addOutput(new TransactionOutput(tx, SATOSHI, outputScript));
            addOnlyInputToTransaction(tx, b33);
            b33.addTransaction(tx);
        }
        b33.solve();

        blocks.add(new BlockAndValidity(b33, true, false, b33.getHash(), chainHeadHeight + 10, "b33"));
        spendableOutputs.offer(b33.getCoinbaseOutput());

        TransactionOutPointWithValue out10 = spendableOutputs.poll();

        NewBlock b34 = createNextBlock(b33, chainHeadHeight + 11, out10, null);
        {
            int sigOps = 0;
            for (Transaction tx : b34.block.getTransactions()) {
                sigOps += tx.getSigOpCount();
            }
            Transaction tx = new Transaction();
            byte[] outputScript = new byte[(Block.MAX_BLOCK_SIGOPS - sigOps)/20 + (Block.MAX_BLOCK_SIGOPS - sigOps)%20 + 1];
            Arrays.fill(outputScript, (byte) OP_CHECKMULTISIGVERIFY);
            for (int i = 0; i < (Block.MAX_BLOCK_SIGOPS - sigOps)%20; i++)
                outputScript[i] = (byte) OP_CHECKSIG;
            tx.addOutput(new TransactionOutput(tx, SATOSHI, outputScript));
            addOnlyInputToTransaction(tx, b34);
            b34.addTransaction(tx);
        }
        b34.solve();
        blocks.add(new BlockAndValidity(b34, false, true, b33.getHash(), chainHeadHeight + 10, "b34"));

        NewBlock b35 = createNextBlock(b33, chainHeadHeight + 11, out10, null);
        {
            int sigOps = 0;
            for (Transaction tx : b35.block.getTransactions()) {
                sigOps += tx.getSigOpCount();
            }
            Transaction tx = new Transaction();
            byte[] outputScript = new byte[Block.MAX_BLOCK_SIGOPS - sigOps];
            Arrays.fill(outputScript, (byte) OP_CHECKSIGVERIFY);
            tx.addOutput(new TransactionOutput(tx, SATOSHI, outputScript));
            addOnlyInputToTransaction(tx, b35);
            b35.addTransaction(tx);
        }
        b35.solve();

        blocks.add(new BlockAndValidity(b35, true, false, b35.getHash(), chainHeadHeight + 11, "b35"));
        spendableOutputs.offer(b35.getCoinbaseOutput());

        TransactionOutPointWithValue out11 = spendableOutputs.poll();

        NewBlock b36 = createNextBlock(b35, chainHeadHeight + 12, out11, null);
        {
            int sigOps = 0;
            for (Transaction tx : b36.block.getTransactions()) {
                sigOps += tx.getSigOpCount();
            }
            Transaction tx = new Transaction();
            byte[] outputScript = new byte[Block.MAX_BLOCK_SIGOPS - sigOps + 1];
            Arrays.fill(outputScript, (byte) OP_CHECKSIGVERIFY);
            tx.addOutput(new TransactionOutput(tx, SATOSHI, outputScript));
            addOnlyInputToTransaction(tx, b36);
            b36.addTransaction(tx);
        }
        b36.solve();

        blocks.add(new BlockAndValidity(b36, false, true, b35.getHash(), chainHeadHeight + 11, "b36"));

        // Check spending of a transaction in a block which failed to connect
        // (test block store transaction abort handling, not that it should get this far if that's broken...)
        // 6  (3)
        // 12 (3) -> b13 (4) -> b15 (5) -> b23 (6) -> b30 (7) -> b31 (8) -> b33 (9) -> b35 (10)
        //                                                                                     \-> b37 (11)
        //                                                                                     \-> b38 (11)
        //
        NewBlock b37 = createNextBlock(b35, chainHeadHeight + 12, out11, null);
        {
            Transaction tx = new Transaction();
            tx.addOutput(new TransactionOutput(tx, SATOSHI, new byte[] {}));
            addOnlyInputToTransaction(tx, out11); // double spend out11
            b37.addTransaction(tx);
        }
        b37.solve();
        blocks.add(new BlockAndValidity(b37, false, true, b35.getHash(), chainHeadHeight + 11, "b37"));

        NewBlock b38 = createNextBlock(b35, chainHeadHeight + 12, out11, null);
        {
            Transaction tx = new Transaction();
            tx.addOutput(new TransactionOutput(tx, SATOSHI, new byte[] {}));
            // Attempt to spend b37's first non-coinbase tx, at which point b37 was still considered valid
            addOnlyInputToTransaction(tx, b37);
            b38.addTransaction(tx);
        }
        b38.solve();
        blocks.add(new BlockAndValidity(b38, false, true, b35.getHash(), chainHeadHeight + 11, "b38"));

        // Check P2SH SigOp counting
        // 13 (4) -> b15 (5) -> b23 (6) -> b30 (7) -> b31 (8) -> b33 (9) -> b35 (10) -> b39 (11) -> b41 (12)
        //                                                                                      \-> b40 (12)
        //
        // Create some P2SH outputs that will require 6 sigops to spend
        byte[] b39p2shScriptPubKey;
        int b39numP2SHOutputs = 0, b39sigOpsPerOutput = 6;
        NewBlock b39 = createNextBlock(b35, chainHeadHeight + 12, null, null);
        {
            ByteArrayOutputStream p2shScriptPubKey = new ByteArrayOutputStream();
            try {
                Script.writeBytes(p2shScriptPubKey, coinbaseOutKeyPubKey);
                p2shScriptPubKey.write(OP_2DUP);
                p2shScriptPubKey.write(OP_CHECKSIGVERIFY);
                p2shScriptPubKey.write(OP_2DUP);
                p2shScriptPubKey.write(OP_CHECKSIGVERIFY);
                p2shScriptPubKey.write(OP_2DUP);
                p2shScriptPubKey.write(OP_CHECKSIGVERIFY);
                p2shScriptPubKey.write(OP_2DUP);
                p2shScriptPubKey.write(OP_CHECKSIGVERIFY);
                p2shScriptPubKey.write(OP_2DUP);
                p2shScriptPubKey.write(OP_CHECKSIGVERIFY);
                p2shScriptPubKey.write(OP_CHECKSIG);
            } catch (IOException e) {
                throw new RuntimeException(e);  // Cannot happen.
            }
            b39p2shScriptPubKey = p2shScriptPubKey.toByteArray();

            byte[] scriptHash = CryptoUtils.sha256hash160(b39p2shScriptPubKey);
            ByteArrayOutputStream scriptPubKey = new ByteArrayOutputStream(scriptHash.length + 3);
            scriptPubKey.write(OP_HASH160);
            try {
                Script.writeBytes(scriptPubKey, scriptHash);
            } catch (IOException e) {
                throw new RuntimeException(e);  // Cannot happen.
            }
            scriptPubKey.write(OP_EQUAL);

            Coin lastOutputValue = out11.value.subtract(SATOSHI);
            TransactionOutPoint lastOutPoint;
            {
                Transaction tx = new Transaction();
                tx.addOutput(new TransactionOutput(tx, SATOSHI, scriptPubKey.toByteArray()));
                tx.addOutput(new TransactionOutput(tx, lastOutputValue, new byte[]{OP_1}));
                addOnlyInputToTransaction(tx, out11);
                lastOutPoint = new TransactionOutPoint(1, tx.getTxId());
                b39.addTransaction(tx);
            }
            b39numP2SHOutputs++;

            while (b39.block.messageSize() < Block.MAX_BLOCK_SIZE)
            {
                Transaction tx = new Transaction();

                lastOutputValue = lastOutputValue.subtract(SATOSHI);
                tx.addOutput(new TransactionOutput(tx, SATOSHI, scriptPubKey.toByteArray()));
                tx.addOutput(new TransactionOutput(tx, lastOutputValue, new byte[]{OP_1}));
                tx.addInput(new TransactionInput(tx, new byte[]{OP_1}, lastOutPoint));
                lastOutPoint = new TransactionOutPoint(1, tx.getTxId());

                if (b39.block.messageSize() + tx.messageSize() < Block.MAX_BLOCK_SIZE) {
                    b39.addTransaction(tx);
                    b39numP2SHOutputs++;
                } else
                    break;
            }
        }
        b39.solve();
        blocks.add(new BlockAndValidity(b39, true, false, b39.getHash(), chainHeadHeight + 12, "b39"));
        spendableOutputs.offer(b39.getCoinbaseOutput());

        TransactionOutPointWithValue out12 = spendableOutputs.poll();

        NewBlock b40 = createNextBlock(b39, chainHeadHeight + 13, out12, null);
        {
            int sigOps = 0;
            for (Transaction tx : b40.block.getTransactions()) {
                sigOps += tx.getSigOpCount();
            }

            int numTxes = (Block.MAX_BLOCK_SIGOPS - sigOps) / b39sigOpsPerOutput;
            checkState(numTxes <= b39numP2SHOutputs);

            TransactionOutPoint lastOutPoint = new TransactionOutPoint(1, b40.block.getTransactions().get(1).getTxId());

            byte[] scriptSig = null;
            for (int i = 1; i <= numTxes; i++) {
                Transaction tx = new Transaction();
                tx.addOutput(new TransactionOutput(tx, SATOSHI, new byte[] {OP_1}));
                tx.addInput(new TransactionInput(tx, new byte[]{OP_1}, lastOutPoint));

                TransactionInput input = new TransactionInput(tx, new byte[]{},
                        new TransactionOutPoint(0, b39.block.getTransactions().get(i).getTxId()));
                tx.addInput(input);

                if (scriptSig == null) {
                    // Exploit the SigHash.SINGLE bug to avoid having to make more than one signature
                    Sha256Hash hash = tx.hashForSignature(1, b39p2shScriptPubKey, SigHash.SINGLE, false);

                    // Sign input
                    try {
                        ByteArrayOutputStream bos = new ByteArrayOutputStream(73);
                        bos.write(coinbaseOutKey.sign(hash).encodeToDER());
                        bos.write(SigHash.SINGLE.value);
                        byte[] signature = bos.toByteArray();

                        ByteArrayOutputStream scriptSigBos = new ByteArrayOutputStream(signature.length + b39p2shScriptPubKey.length + 3);
                        Script.writeBytes(scriptSigBos, new byte[] {(byte) OP_CHECKSIG});
                        scriptSigBos.write(Script.createInputScript(signature));
                        Script.writeBytes(scriptSigBos, b39p2shScriptPubKey);

                        scriptSig = scriptSigBos.toByteArray();
                    } catch (IOException e) {
                        throw new RuntimeException(e);  // Cannot happen.
                    }
                }

                input.setScriptBytes(scriptSig);

                lastOutPoint = new TransactionOutPoint(0, tx.getTxId());

                b40.addTransaction(tx);
            }

            sigOps += numTxes * b39sigOpsPerOutput;
            Transaction tx = new Transaction();
            tx.addInput(new TransactionInput(tx, new byte[]{OP_1}, lastOutPoint));
            byte[] scriptPubKey = new byte[Block.MAX_BLOCK_SIGOPS - sigOps + 1];
            Arrays.fill(scriptPubKey, (byte) OP_CHECKSIG);
            tx.addOutput(new TransactionOutput(tx, ZERO, scriptPubKey));
            b40.addTransaction(tx);
        }
        b40.solve();
        blocks.add(new BlockAndValidity(b40, false, true, b39.getHash(), chainHeadHeight + 12, "b40"));

        NewBlock b41 = null;
        if (runBarelyExpensiveTests) {
            b41 = createNextBlock(b39, chainHeadHeight + 13, out12, null);
            {
                int sigOps = 0;
                for (Transaction tx : b41.block.getTransactions()) {
                    sigOps += tx.getSigOpCount();
                }

                int numTxes = (Block.MAX_BLOCK_SIGOPS - sigOps)
                        / b39sigOpsPerOutput;
                checkState(numTxes <= b39numP2SHOutputs);

                TransactionOutPoint lastOutPoint = new TransactionOutPoint(
                        1, b41.block.getTransactions().get(1).getTxId());

                byte[] scriptSig = null;
                for (int i = 1; i <= numTxes; i++) {
                    Transaction tx = new Transaction();
                    tx.addOutput(new TransactionOutput(tx, Coin
                            .SATOSHI, new byte[] {OP_1}));
                    tx.addInput(new TransactionInput(tx, new byte[] { OP_1 }, lastOutPoint));

                    TransactionInput input = new TransactionInput(tx, new byte[] {},
                            new TransactionOutPoint(0, b39.block.getTransactions().get(i).getTxId()));
                    tx.addInput(input);

                    if (scriptSig == null) {
                        // Exploit the SigHash.SINGLE bug to avoid having to make more than one signature
                        Sha256Hash hash = tx.hashForSignature(1,
                                b39p2shScriptPubKey, SigHash.SINGLE, false);

                        // Sign input
                        try {
                            ByteArrayOutputStream bos = new ByteArrayOutputStream(
                                    73);
                            bos.write(coinbaseOutKey.sign(hash).encodeToDER());
                            bos.write(SigHash.SINGLE.value);
                            byte[] signature = bos.toByteArray();

                            ByteArrayOutputStream scriptSigBos = new ByteArrayOutputStream(
                                    signature.length
                                            + b39p2shScriptPubKey.length + 3);
                            Script.writeBytes(scriptSigBos,
                                    new byte[] { (byte) OP_CHECKSIG});
                            scriptSigBos.write(Script
                                    .createInputScript(signature));
                            Script.writeBytes(scriptSigBos, b39p2shScriptPubKey);

                            scriptSig = scriptSigBos.toByteArray();
                        } catch (IOException e) {
                            throw new RuntimeException(e); // Cannot happen.
                        }
                    }

                    input.setScriptBytes(scriptSig);

                    lastOutPoint = new TransactionOutPoint(0,
                            tx.getTxId());

                    b41.addTransaction(tx);
                }

                sigOps += numTxes * b39sigOpsPerOutput;
                Transaction tx = new Transaction();
                tx.addInput(new TransactionInput(tx, new byte[] { OP_1 }, lastOutPoint));
                byte[] scriptPubKey = new byte[Block.MAX_BLOCK_SIGOPS - sigOps];
                Arrays.fill(scriptPubKey, (byte) OP_CHECKSIG);
                tx.addOutput(new TransactionOutput(tx, ZERO, scriptPubKey));
                b41.addTransaction(tx);
            }
            b41.solve();
            blocks.add(new BlockAndValidity(b41, true, false, b41.getHash(), chainHeadHeight + 13, "b41"));
        }

        // Fork off of b39 to create a constant base again
        // b23 (6) -> b30 (7) -> b31 (8) -> b33 (9) -> b35 (10) -> b39 (11) -> b42 (12) -> b43 (13)
        //                                                                 \-> b41 (12)
        //
        NewBlock b42 = createNextBlock(b39, chainHeadHeight + 13, out12, null);
        blocks.add(new BlockAndValidity(b42, true, false, b41 == null ? b42.getHash() : b41.getHash(), chainHeadHeight + 13, "b42"));
        spendableOutputs.offer(b42.getCoinbaseOutput());

        TransactionOutPointWithValue out13 = spendableOutputs.poll();

        NewBlock b43 = createNextBlock(b42, chainHeadHeight + 14, out13, null);
        blocks.add(new BlockAndValidity(b43, true, false, b43.getHash(), chainHeadHeight + 14, "b43"));
        spendableOutputs.offer(b43.getCoinbaseOutput());

        // Test a number of really invalid scenarios
        //  -> b31 (8) -> b33 (9) -> b35 (10) -> b39 (11) -> b42 (12) -> b43 (13) -> b44 (14)
        //                                                                                   \-> ??? (15)
        //
        TransactionOutPointWithValue out14 = spendableOutputs.poll();

        // A valid block created exactly like b44 to make sure the creation itself works
        Block b44 = new Block(Block.BLOCK_VERSION_GENESIS);
        byte[] outScriptBytes = ScriptBuilder.createP2PKOutputScript(ECKey.fromPublicOnly(coinbaseOutKeyPubKey)).program();
        {
            b44.setDifficultyTarget(b43.block.getDifficultyTarget());
            b44.addCoinbaseTransaction(coinbaseOutKeyPubKey, ZERO, chainHeadHeight + 15);

            Transaction t = new Transaction();
            // Entirely invalid scriptPubKey to ensure we aren't pre-verifying too much
            t.addOutput(new TransactionOutput(t, ZERO, new byte[] {OP_PUSHDATA1 - 1 }));
            t.addOutput(new TransactionOutput(t, SATOSHI, outScriptBytes));
            // Spendable output
            t.addOutput(new TransactionOutput(t, ZERO, new byte[] {OP_1}));
            addOnlyInputToTransaction(t, out14);
            b44.addTransaction(t);

            b44.setPrevBlockHash(b43.getHash());
            b44.setTime(b43.block.time().plusSeconds(1));
        }
        b44.solve();
        blocks.add(new BlockAndValidity(b44, true, false, b44.getHash(), chainHeadHeight + 15, "b44"));

        TransactionOutPointWithValue out15 = spendableOutputs.poll();

        // A block with a non-coinbase as the first tx
        Block b45 = new Block(Block.BLOCK_VERSION_GENESIS);
        {
            b45.setDifficultyTarget(b44.getDifficultyTarget());
            //b45.addCoinbaseTransaction(pubKey, coinbaseValue);

            Transaction t = new Transaction();
            // Entirely invalid scriptPubKey to ensure we aren't pre-verifying too much
            t.addOutput(new TransactionOutput(t, ZERO, new byte[] {OP_PUSHDATA1 - 1 }));
            t.addOutput(new TransactionOutput(t, SATOSHI, outScriptBytes));
            // Spendable output
            t.addOutput(new TransactionOutput(t, ZERO, new byte[] {OP_1}));
            addOnlyInputToTransaction(t, out15);
            try {
                b45.addTransaction(t);
            } catch (RuntimeException e) { } // Should happen
            if (b45.getTransactions().size() > 0)
                throw new RuntimeException("addTransaction doesn't properly check for adding a non-coinbase as first tx");
            b45.addTransaction(t, false);

            b45.setPrevBlockHash(b44.getHash());
            b45.setTime(b44.time().plusSeconds(1));
        }
        b45.solve();
        blocks.add(new BlockAndValidity(b45, false, true, b44.getHash(), chainHeadHeight + 15, "b45"));

        // A block with no txn
        Block b46 = new Block(Block.BLOCK_VERSION_GENESIS);
        {
            b46.transactions = new ArrayList<>();
            b46.setDifficultyTarget(b44.getDifficultyTarget());
            b46.setMerkleRoot(Sha256Hash.ZERO_HASH);

            b46.setPrevBlockHash(b44.getHash());
            b46.setTime(b44.time().plusSeconds(1));
        }
        b46.solve();
        blocks.add(new BlockAndValidity(b46, false, true, b44.getHash(), chainHeadHeight + 15, "b46"));

        // A block with invalid work
        NewBlock b47 = createNextBlock(b44, chainHeadHeight + 16, out15, null);
        {
            try {
                // Inverse solve
                BigInteger target = b47.block.getDifficultyTargetAsInteger();
                while (true) {
                    BigInteger h = b47.getHash().toBigInteger();
                    if (h.compareTo(target) > 0) // if invalid
                        break;
                    // increment the nonce and try again.
                    b47.block.setNonce(b47.block.getNonce() + 1);
                }
            } catch (VerificationException e) {
                throw new RuntimeException(e); // Cannot happen.
            }
        }
        blocks.add(new BlockAndValidity(b47, false, true, b44.getHash(), chainHeadHeight + 15, "b47"));

        // Block with timestamp > 2h in the future
        NewBlock b48 = createNextBlock(b44, chainHeadHeight + 16, out15, null);
        b48.block.setTime(TimeUtils.currentTime().plusSeconds(60 * 60 * 3));
        b48.solve();
        blocks.add(new BlockAndValidity(b48, false, true, b44.getHash(), chainHeadHeight + 15, "b48"));

        // Block with invalid merkle hash
        NewBlock b49 = createNextBlock(b44, chainHeadHeight + 16, out15, null);
        byte[] b49MerkleHash = Sha256Hash.ZERO_HASH.getBytes().clone();
        b49MerkleHash[1] = (byte) 0xDE;
        b49.block.setMerkleRoot(Sha256Hash.of(b49MerkleHash));
        b49.solve();
        blocks.add(new BlockAndValidity(b49, false, true, b44.getHash(), chainHeadHeight + 15, "b49"));

        // Block with incorrect POW limit
        NewBlock b50 = createNextBlock(b44, chainHeadHeight + 16, out15, null);
        {
            long diffTarget = b44.getDifficultyTarget();
            diffTarget &= 0xFFBFFFFF; // Make difficulty one bit harder
            b50.block.setDifficultyTarget(diffTarget);
        }
        b50.solve();
        blocks.add(new BlockAndValidity(b50, false, true, b44.getHash(), chainHeadHeight + 15, "b50"));

        // A block with two coinbase txn
        NewBlock b51 = createNextBlock(b44, chainHeadHeight + 16, out15, null);
        {
            Transaction coinbase = new Transaction();
            coinbase.addInput(TransactionInput.coinbaseInput(coinbase, new byte[]{(byte) 0xff, 110, 1}));
            coinbase.addOutput(new TransactionOutput(coinbase, SATOSHI, outScriptBytes));
            b51.block.addTransaction(coinbase, false);
        }
        b51.solve();
        blocks.add(new BlockAndValidity(b51, false, true, b44.getHash(), chainHeadHeight + 15, "b51"));

        // A block with duplicate txn
        NewBlock b52 = createNextBlock(b44, chainHeadHeight + 16, out15, null);
        {
            Transaction tx = new Transaction();
            tx.addOutput(new TransactionOutput(tx, SATOSHI, new byte[] {}));
            addOnlyInputToTransaction(tx, b52);
            b52.addTransaction(tx);
            b52.addTransaction(tx);
        }
        b52.solve();
        blocks.add(new BlockAndValidity(b52, false, true, b44.getHash(), chainHeadHeight + 15, "b52"));

        // Test block timestamp
        //  -> b31 (8) -> b33 (9) -> b35 (10) -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15)
        //                                                                                   \-> b54 (15)
        //                                                                       \-> b44 (14)
        //
        NewBlock b53 = createNextBlock(b43, chainHeadHeight + 15, out14, null);
        blocks.add(new BlockAndValidity(b53, true, false, b44.getHash(), chainHeadHeight + 15, "b53"));
        spendableOutputs.offer(b53.getCoinbaseOutput());

        // Block with invalid timestamp
        NewBlock b54 = createNextBlock(b53, chainHeadHeight + 16, out15, null);
        b54.block.setTime(b35.block.time().minusSeconds(1));
        b54.solve();
        blocks.add(new BlockAndValidity(b54, false, true, b44.getHash(), chainHeadHeight + 15, "b54"));

        // Block with valid timestamp
        NewBlock b55 = createNextBlock(b53, chainHeadHeight + 16, out15, null);
        b55.block.setTime(b35.block.time());
        b55.solve();
        blocks.add(new BlockAndValidity(b55, true, false, b55.getHash(), chainHeadHeight + 16, "b55"));
        spendableOutputs.offer(b55.getCoinbaseOutput());

        // Test CVE-2012-2459
        // -> b33 (9) -> b35 (10) -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16)
        //                                                                                   \-> b56 (16)
        //
        TransactionOutPointWithValue out16 = spendableOutputs.poll();

        NewBlock b57 = createNextBlock(b55, chainHeadHeight + 17, out16, null);
        Transaction b56txToDuplicate;
        {
            b56txToDuplicate = new Transaction();
            b56txToDuplicate.addOutput(new TransactionOutput(b56txToDuplicate, SATOSHI, new byte[] {}));
            addOnlyInputToTransaction(b56txToDuplicate, b57);
            b57.addTransaction(b56txToDuplicate);
        }
        b57.solve();

        Block b56;
        try {
            b56 = params.getDefaultSerializer().makeBlock(ByteBuffer.wrap(b57.block.serialize()));
        } catch (ProtocolException e) {
            throw new RuntimeException(e); // Cannot happen.
        }
        b56.addTransaction(b56txToDuplicate);
        checkState(b56.getHash().equals(b57.getHash()));
        blocks.add(new BlockAndValidity(b56, false, true, b55.getHash(), chainHeadHeight + 16, "b56"));

        NewBlock b57p2 = createNextBlock(b55, chainHeadHeight + 17, out16, null);
        Transaction b56p2txToDuplicate1, b56p2txToDuplicate2;
        {
            Transaction tx1 = new Transaction();
            tx1.addOutput(new TransactionOutput(tx1, SATOSHI, new byte[] {OP_TRUE}));
            addOnlyInputToTransaction(tx1, b57p2);
            b57p2.addTransaction(tx1);

            Transaction tx2 = new Transaction();
            tx2.addOutput(new TransactionOutput(tx2, SATOSHI, new byte[] {OP_TRUE}));
            addOnlyInputToTransaction(tx2, new TransactionOutPointWithValue(
                    new TransactionOutPoint(0, tx1.getTxId()),
                    SATOSHI, tx1.getOutput(0).getScriptPubKey()));
            b57p2.addTransaction(tx2);

            b56p2txToDuplicate1 = new Transaction();
            b56p2txToDuplicate1.addOutput(new TransactionOutput(b56p2txToDuplicate1, SATOSHI, new byte[]{OP_TRUE}));
            addOnlyInputToTransaction(b56p2txToDuplicate1, new TransactionOutPointWithValue(
                    new TransactionOutPoint(0, tx2.getTxId()),
                    SATOSHI, tx2.getOutput(0).getScriptPubKey()));
            b57p2.addTransaction(b56p2txToDuplicate1);

            b56p2txToDuplicate2 = new Transaction();
            b56p2txToDuplicate2.addOutput(new TransactionOutput(b56p2txToDuplicate2, SATOSHI, new byte[]{}));
            addOnlyInputToTransaction(b56p2txToDuplicate2, new TransactionOutPointWithValue(
                    new TransactionOutPoint(0, b56p2txToDuplicate1.getTxId()),
                    SATOSHI, b56p2txToDuplicate1.getOutput(0).getScriptPubKey()));
            b57p2.addTransaction(b56p2txToDuplicate2);
        }
        b57p2.solve();

        Block b56p2;
        try {
            b56p2 = params.getDefaultSerializer().makeBlock(ByteBuffer.wrap(b57p2.block.serialize()));
        } catch (ProtocolException e) {
            throw new RuntimeException(e); // Cannot happen.
        }
        b56p2.addTransaction(b56p2txToDuplicate1);
        b56p2.addTransaction(b56p2txToDuplicate2);
        checkState(b56p2.getHash().equals(b57p2.getHash()));
        blocks.add(new BlockAndValidity(b56p2, false, true, b55.getHash(), chainHeadHeight + 16, "b56p2"));
        blocks.add(new BlockAndValidity(b57p2, true, false, b57p2.getHash(), chainHeadHeight + 17, "b57p2"));

        blocks.add(new BlockAndValidity(b57, true, false, b57p2.getHash(), chainHeadHeight + 17, "b57"));
        spendableOutputs.offer(b57.getCoinbaseOutput());

        // Test a few invalid tx types
        // -> b35 (10) -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17)
        //                                                                                    \-> ??? (17)
        //
        TransactionOutPointWithValue out17 = spendableOutputs.poll();

        // tx with prevout.n out of range
        NewBlock b58 = createNextBlock(b57, chainHeadHeight + 18, out17, null);
        {
            Transaction tx = new Transaction();
            tx.addOutput(new TransactionOutput(tx, ZERO, new byte[] {}));
            // Replace the TransactionOutPoint with an out-of-range OutPoint
            b58.getSpendableOutput().outpoint = new TransactionOutPoint(42, tx);
            addOnlyInputToTransaction(tx, b58);
            b58.addTransaction(tx);
        }
        b58.solve();
        blocks.add(new BlockAndValidity(b58, false, true, b57p2.getHash(), chainHeadHeight + 17, "b58"));

        // tx with output value > input value out of range
        NewBlock b59 = createNextBlock(b57, chainHeadHeight + 18, out17, null);
        {
            Transaction tx = new Transaction();
            tx.addOutput(new TransactionOutput(tx,
                    b59.getSpendableOutput().value.add(SATOSHI), new byte[]{}));
            addOnlyInputToTransaction(tx, b59);
            b59.addTransaction(tx);
        }
        b59.solve();
        blocks.add(new BlockAndValidity(b59, false, true, b57p2.getHash(), chainHeadHeight + 17, "b59"));

        NewBlock b60 = createNextBlock(b57, chainHeadHeight + 18, out17, null);
        blocks.add(new BlockAndValidity(b60, true, false, b60.getHash(), chainHeadHeight + 18, "b60"));
        spendableOutputs.offer(b60.getCoinbaseOutput());

        // Test BIP30
        // -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17)
        //                                                                                    \-> b61 (18)
        //
        TransactionOutPointWithValue out18 = spendableOutputs.poll();

        NewBlock b61 = createNextBlock(b60, chainHeadHeight + 19, out18, null);
        {
            b61.block.getTransactions().get(0).getInput(0).setScriptBytes(b60.block.getTransactions().get(0).getInput(0).getScriptBytes());
            b61.block.unCache();
            checkState(b61.block.getTransactions().get(0).equals(b60.block.getTransactions().get(0)));
        }
        b61.solve();
        blocks.add(new BlockAndValidity(b61, false, true, b60.getHash(), chainHeadHeight + 18, "b61"));

        // Test tx.isFinal is properly rejected (not an exhaustive tx.isFinal test, that should be in data-driven transaction tests)
        // -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17)
        //                                                                                    \-> b62 (18)
        //
        NewBlock b62 = createNextBlock(b60, chainHeadHeight + 19, null, null);
        {
            Transaction tx = new Transaction();
            tx.setLockTime(0xffffffffL);
            tx.addOutput(ZERO, OP_TRUE_SCRIPT);
            addOnlyInputToTransaction(tx, out18, 0);
            b62.addTransaction(tx);
            checkState(!tx.isFinal(chainHeadHeight + 17, b62.block.getTimeSeconds()));
        }
        b62.solve();
        blocks.add(new BlockAndValidity(b62, false, true, b60.getHash(), chainHeadHeight + 18, "b62"));

        // Test a non-final coinbase is also rejected
        // -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17)
        //                                                                                    \-> b63 (-)
        //
        NewBlock b63 = createNextBlock(b60, chainHeadHeight + 19, null, null);
        {
            b63.block.getTransactions().get(0).setLockTime(0xffffffffL);
            b63.block.getTransactions().get(0).getInput(0).setSequenceNumber(0xdeadbeefL);
            checkState(!b63.block.getTransactions().get(0).isFinal(chainHeadHeight + 17, b63.block.getTimeSeconds()));
        }
        b63.solve();
        blocks.add(new BlockAndValidity(b63, false, true, b60.getHash(), chainHeadHeight + 18, "b63"));

        // Check that a block which is (when properly encoded) <= MAX_BLOCK_SIZE is accepted
        // Even when it is encoded with varints that make its encoded size actually > MAX_BLOCK_SIZE
        // -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18)
        //
        Block b64; NewBlock b64Original;
        {
            b64Original = createNextBlock(b60, chainHeadHeight + 19, out18, null);
            Transaction tx = new Transaction();
            byte[] outputScript = new byte[Block.MAX_BLOCK_SIZE - b64Original.block.messageSize() - 65];
            Arrays.fill(outputScript, (byte) OP_FALSE);
            tx.addOutput(new TransactionOutput(tx, ZERO, outputScript));
            addOnlyInputToTransaction(tx, b64Original);
            b64Original.addTransaction(tx);
            b64Original.solve();
            checkState(b64Original.block.messageSize() == Block.MAX_BLOCK_SIZE);

            ByteArrayOutputStream stream = new ByteArrayOutputStream(b64Original.block.messageSize() + 8);
            b64Original.block.writeHeader(stream);

            byte[] varIntBytes = new byte[9];
            varIntBytes[0] = (byte) 255;
            ByteUtils.writeInt64LE(b64Original.block.getTransactions().size(), varIntBytes, 1);
            stream.write(varIntBytes);
            checkState(VarInt.ofBytes(varIntBytes, 0).intValue() == b64Original.block.getTransactions().size());

            for (Transaction transaction : b64Original.block.getTransactions())
                transaction.bitcoinSerializeToStream(stream);
            b64 = params.getSerializer().makeBlock(ByteBuffer.wrap(stream.toByteArray()));

            // The following checks are checking to ensure block serialization functions in the way needed for this test
            // If they fail, it is likely not an indication of error, but an indication that this test needs rewritten
            checkState(stream.size() == b64Original.block.messageSize() + 8);
            // This check fails because it was created for "retain mode" and the likely encoding is not "optimal".
            // We since removed this capability retain the original encoding, but could not rewrite this test data.
            // checkState(stream.size() == b64.getMessageSize());
            // checkState(Arrays.equals(stream.toByteArray(), b64.bitcoinSerialize()));
            // checkState(b64.getOptimalEncodingMessageSize() == b64Original.block.getMessageSize());
        }
        blocks.add(new BlockAndValidity(b64, true, false, b64.getHash(), chainHeadHeight + 19, "b64"));
        spendableOutputs.offer(b64Original.getCoinbaseOutput());

        // Spend an output created in the block itself
        // -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19)
        //
        TransactionOutPointWithValue out19 = spendableOutputs.poll();  checkState(out19 != null);//TODO preconditions all the way up

        NewBlock b65 = createNextBlock(b64, chainHeadHeight + 20, null, null);
        {
            Transaction tx1 = new Transaction();
            tx1.addOutput(out19.value, OP_TRUE_SCRIPT);
            addOnlyInputToTransaction(tx1, out19, 0);
            b65.addTransaction(tx1);
            Transaction tx2 = new Transaction();
            tx2.addOutput(ZERO, OP_TRUE_SCRIPT);
            tx2.addInput(tx1.getTxId(), 0, OP_TRUE_SCRIPT);
            b65.addTransaction(tx2);
        }
        b65.solve();
        blocks.add(new BlockAndValidity(b65, true, false, b65.getHash(), chainHeadHeight + 20, "b65"));
        spendableOutputs.offer(b65.getCoinbaseOutput());

        // Attempt to spend an output created later in the same block
        // -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19)
        //                                                                                    \-> b66 (20)
        //
        TransactionOutPointWithValue out20 = spendableOutputs.poll();  checkState(out20 != null);

        NewBlock b66 = createNextBlock(b65, chainHeadHeight + 21, null, null);
        {
            Transaction tx1 = new Transaction();
            tx1.addOutput(out20.value, OP_TRUE_SCRIPT);
            addOnlyInputToTransaction(tx1, out20, 0);
            Transaction tx2 = new Transaction();
            tx2.addOutput(ZERO, OP_TRUE_SCRIPT);
            tx2.addInput(tx1.getTxId(), 0, OP_NOP_SCRIPT);
            b66.addTransaction(tx2);
            b66.addTransaction(tx1);
        }
        b66.solve();
        blocks.add(new BlockAndValidity(b66, false, true, b65.getHash(), chainHeadHeight + 20, "b66"));

        // Attempt to double-spend a transaction created in a block
        // -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19)
        //                                                                                    \-> b67 (20)
        //
        NewBlock b67 = createNextBlock(b65, chainHeadHeight + 21, null, null);
        {
            Transaction tx1 = new Transaction();
            tx1.addOutput(out20.value, OP_TRUE_SCRIPT);
            addOnlyInputToTransaction(tx1, out20, 0);
            b67.addTransaction(tx1);
            Transaction tx2 = new Transaction();
            tx2.addOutput(ZERO, OP_TRUE_SCRIPT);
            tx2.addInput(tx1.getTxId(), 0, OP_NOP_SCRIPT);
            b67.addTransaction(tx2);
            Transaction tx3 = new Transaction();
            tx3.addOutput(out20.value, OP_TRUE_SCRIPT);
            tx3.addInput(tx1.getTxId(), 0, OP_NOP_SCRIPT);
            b67.addTransaction(tx3);
        }
        b67.solve();
        blocks.add(new BlockAndValidity(b67, false, true, b65.getHash(), chainHeadHeight + 20, "b67"));

        // A few more tests of block subsidy
        // -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) -> b69 (20)
        //                                                                                    \-> b68 (20)
        //
        NewBlock b68 = createNextBlock(b65, chainHeadHeight + 21, null, SATOSHI.multiply(10));
        {
            Transaction tx = new Transaction();
            tx.addOutput(out20.value.subtract(Coin.valueOf(9)), OP_TRUE_SCRIPT);
            addOnlyInputToTransaction(tx, out20, 0);
            b68.addTransaction(tx);
        }
        b68.solve();
        blocks.add(new BlockAndValidity(b68, false, true, b65.getHash(), chainHeadHeight + 20, "b68"));

        NewBlock b69 = createNextBlock(b65, chainHeadHeight + 21, null, SATOSHI.multiply(10));
        {
            Transaction tx = new Transaction();
            tx.addOutput(out20.value.subtract(Coin.valueOf(10)), OP_TRUE_SCRIPT);
            addOnlyInputToTransaction(tx, out20, 0);
            b69.addTransaction(tx);
        }
        b69.solve();
        blocks.add(new BlockAndValidity(b69, true, false, b69.getHash(), chainHeadHeight + 21, "b69"));
        spendableOutputs.offer(b69.getCoinbaseOutput());

        // Test spending the outpoint of a non-existent transaction
        // -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) -> b69 (20)
        //                                                                                    \-> b70 (21)
        //
        TransactionOutPointWithValue out21 = spendableOutputs.poll();  checkState(out21 != null);
        NewBlock b70 = createNextBlock(b69, chainHeadHeight + 22, out21, null);
        {
            Transaction tx = new Transaction();
            tx.addOutput(ZERO, OP_TRUE_SCRIPT);
            tx.addInput(Sha256Hash.wrap("23c70ed7c0506e9178fc1a987f40a33946d4ad4c962b5ae3a52546da53af0c5c"), 0,
                    OP_NOP_SCRIPT);
            b70.addTransaction(tx);
        }
        b70.solve();
        blocks.add(new BlockAndValidity(b70, false, true, b69.getHash(), chainHeadHeight + 21, "b70"));

        // Test accepting an invalid block which has the same hash as a valid one (via merkle tree tricks)
        // -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) -> b69 (20) -> b71 (21)
        //                                                                                    \-> b72 (21)
        //
        NewBlock b72 = createNextBlock(b69, chainHeadHeight + 22, out21, null);
        {
            Transaction tx = new Transaction();
            tx.addOutput(ZERO, OP_TRUE_SCRIPT);
            addOnlyInputToTransaction(tx, b72);
            b72.addTransaction(tx);
        }
        b72.solve();

        Block b71 = params.getDefaultSerializer().makeBlock(ByteBuffer.wrap(b72.block.serialize()));
        b71.addTransaction(b72.block.getTransactions().get(2));
        checkState(b71.getHash().equals(b72.getHash()));
        blocks.add(new BlockAndValidity(b71, false, true, b69.getHash(), chainHeadHeight + 21, "b71"));
        blocks.add(new BlockAndValidity(b72, true, false, b72.getHash(), chainHeadHeight + 22, "b72"));
        spendableOutputs.offer(b72.getCoinbaseOutput());

        // Have some fun with invalid scripts and MAX_BLOCK_SIGOPS
        // -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) -> b69 (20) -> b72 (21)
        //                                                                                    \-> b** (22)
        //
        TransactionOutPointWithValue out22 = spendableOutputs.poll();  checkState(out22 != null);

        NewBlock b73 = createNextBlock(b72, chainHeadHeight + 23, out22, null);
        {
            int sigOps = 0;
            for (Transaction tx : b73.block.getTransactions()) {
                sigOps += tx.getSigOpCount();
            }
            Transaction tx = new Transaction();
            byte[] outputScript = new byte[Block.MAX_BLOCK_SIGOPS - sigOps + Script.MAX_SCRIPT_ELEMENT_SIZE + 1 + 5 + 1];
            Arrays.fill(outputScript, (byte) OP_CHECKSIG);
            // If we push an element that is too large, the CHECKSIGs after that push are still counted
            outputScript[Block.MAX_BLOCK_SIGOPS - sigOps] = OP_PUSHDATA4;
            ByteUtils.writeInt32LE(Script.MAX_SCRIPT_ELEMENT_SIZE + 1, outputScript, Block.MAX_BLOCK_SIGOPS - sigOps + 1);
            tx.addOutput(new TransactionOutput(tx, SATOSHI, outputScript));
            addOnlyInputToTransaction(tx, b73);
            b73.addTransaction(tx);
        }
        b73.solve();
        blocks.add(new BlockAndValidity(b73, false, true, b72.getHash(), chainHeadHeight + 22, "b73"));

        NewBlock b74 = createNextBlock(b72, chainHeadHeight + 23, out22, null);
        {
            int sigOps = 0;
            for (Transaction tx : b74.block.getTransactions()) {
                sigOps += tx.getSigOpCount();
            }
            Transaction tx = new Transaction();
            byte[] outputScript = new byte[Block.MAX_BLOCK_SIGOPS - sigOps + Script.MAX_SCRIPT_ELEMENT_SIZE + 42];
            Arrays.fill(outputScript, (byte) OP_CHECKSIG);
            // If we push an invalid element, all previous CHECKSIGs are counted
            outputScript[Block.MAX_BLOCK_SIGOPS - sigOps + 1] = OP_PUSHDATA4;
            outputScript[Block.MAX_BLOCK_SIGOPS - sigOps + 2] = (byte)0xfe;
            outputScript[Block.MAX_BLOCK_SIGOPS - sigOps + 3] = (byte)0xff;
            outputScript[Block.MAX_BLOCK_SIGOPS - sigOps + 4] = (byte)0xff;
            outputScript[Block.MAX_BLOCK_SIGOPS - sigOps + 5] = (byte)0xff;
            tx.addOutput(new TransactionOutput(tx, SATOSHI, outputScript));
            addOnlyInputToTransaction(tx, b74);
            b74.addTransaction(tx);
        }
        b74.solve();
        blocks.add(new BlockAndValidity(b74, false, true, b72.getHash(), chainHeadHeight + 22, "b74"));

        NewBlock b75 = createNextBlock(b72, chainHeadHeight + 23, out22, null);
        {
            int sigOps = 0;
            for (Transaction tx : b75.block.getTransactions()) {
                sigOps += tx.getSigOpCount();
            }
            Transaction tx = new Transaction();
            byte[] outputScript = new byte[Block.MAX_BLOCK_SIGOPS - sigOps + Script.MAX_SCRIPT_ELEMENT_SIZE + 42];
            Arrays.fill(outputScript, (byte) OP_CHECKSIG);
            // If we push an invalid element, all subsequent CHECKSIGs are not counted
            outputScript[Block.MAX_BLOCK_SIGOPS - sigOps] = OP_PUSHDATA4;
            outputScript[Block.MAX_BLOCK_SIGOPS - sigOps + 1] = (byte)0xff;
            outputScript[Block.MAX_BLOCK_SIGOPS - sigOps + 2] = (byte)0xff;
            outputScript[Block.MAX_BLOCK_SIGOPS - sigOps + 3] = (byte)0xff;
            outputScript[Block.MAX_BLOCK_SIGOPS - sigOps + 4] = (byte)0xff;
            tx.addOutput(new TransactionOutput(tx, SATOSHI, outputScript));
            addOnlyInputToTransaction(tx, b75);
            b75.addTransaction(tx);
        }
        b75.solve();
        blocks.add(new BlockAndValidity(b75, true, false, b75.getHash(), chainHeadHeight + 23, "b75"));
        spendableOutputs.offer(b75.getCoinbaseOutput());

        TransactionOutPointWithValue out23 = spendableOutputs.poll();  checkState(out23 != null);

        NewBlock b76 = createNextBlock(b75, chainHeadHeight + 24, out23, null);
        {
            int sigOps = 0;
            for (Transaction tx : b76.block.getTransactions()) {
                sigOps += tx.getSigOpCount();
            }
            Transaction tx = new Transaction();
            byte[] outputScript = new byte[Block.MAX_BLOCK_SIGOPS - sigOps + Script.MAX_SCRIPT_ELEMENT_SIZE + 1 + 5];
            Arrays.fill(outputScript, (byte) OP_CHECKSIG);
            // If we push an element that is filled with CHECKSIGs, they (obviously) arent counted
            outputScript[Block.MAX_BLOCK_SIGOPS - sigOps] = OP_PUSHDATA4;
            ByteUtils.writeInt32LE(Block.MAX_BLOCK_SIGOPS, outputScript, Block.MAX_BLOCK_SIGOPS - sigOps + 1);
            tx.addOutput(new TransactionOutput(tx, SATOSHI, outputScript));
            addOnlyInputToTransaction(tx, b76);
            b76.addTransaction(tx);
        }
        b76.solve();
        blocks.add(new BlockAndValidity(b76, true, false, b76.getHash(), chainHeadHeight + 24, "b76"));
        spendableOutputs.offer(b76.getCoinbaseOutput());

        // Test transaction resurrection
        // -> b77 (24) -> b78 (25) -> b79 (26)
        //            \-> b80 (25) -> b81 (26) -> b82 (27)
        // b78 creates a tx, which is spent in b79. after b82, both should be in mempool
        //
        TransactionOutPointWithValue out24 = Objects.requireNonNull(spendableOutputs.poll());
        TransactionOutPointWithValue out25 = Objects.requireNonNull(spendableOutputs.poll());
        TransactionOutPointWithValue out26 = Objects.requireNonNull(spendableOutputs.poll());
        TransactionOutPointWithValue out27 = Objects.requireNonNull(spendableOutputs.poll());

        NewBlock b77 = createNextBlock(b76, chainHeadHeight + 25, out24, null);
        blocks.add(new BlockAndValidity(b77, true, false, b77.getHash(), chainHeadHeight + 25, "b77"));
        spendableOutputs.offer(b77.getCoinbaseOutput());

        NewBlock b78 = createNextBlock(b77, chainHeadHeight + 26, out25, null);
        Transaction b78tx = new Transaction();
        {
            b78tx.addOutput(ZERO, OP_TRUE_SCRIPT);
            addOnlyInputToTransaction(b78tx, b77);
            b78.addTransaction(b78tx);
        }
        b78.solve();
        blocks.add(new BlockAndValidity(b78, true, false, b78.getHash(), chainHeadHeight + 26, "b78"));

        NewBlock b79 = createNextBlock(b78, chainHeadHeight + 27, out26, null);
        Transaction b79tx = new Transaction();

        {
            b79tx.addOutput(ZERO, OP_TRUE_SCRIPT);
            b79tx.addInput(b78tx.getTxId(), 0, OP_NOP_SCRIPT);
            b79.addTransaction(b79tx);
        }
        b79.solve();
        blocks.add(new BlockAndValidity(b79, true, false, b79.getHash(), chainHeadHeight + 27, "b79"));

        blocks.add(new MemoryPoolState(new HashSet<InventoryItem>(), "post-b79 empty mempool"));

        NewBlock b80 = createNextBlock(b77, chainHeadHeight + 26, out25, null);
        blocks.add(new BlockAndValidity(b80, true, false, b79.getHash(), chainHeadHeight + 27, "b80"));
        spendableOutputs.offer(b80.getCoinbaseOutput());

        NewBlock b81 = createNextBlock(b80, chainHeadHeight + 27, out26, null);
        blocks.add(new BlockAndValidity(b81, true, false, b79.getHash(), chainHeadHeight + 27, "b81"));
        spendableOutputs.offer(b81.getCoinbaseOutput());

        NewBlock b82 = createNextBlock(b81, chainHeadHeight + 28, out27, null);
        blocks.add(new BlockAndValidity(b82, true, false, b82.getHash(), chainHeadHeight + 28, "b82"));
        spendableOutputs.offer(b82.getCoinbaseOutput());

        HashSet<InventoryItem> post82Mempool = new HashSet<>();
        post82Mempool.add(new InventoryItem(InventoryItem.Type.TRANSACTION, b78tx.getTxId()));
        post82Mempool.add(new InventoryItem(InventoryItem.Type.TRANSACTION, b79tx.getTxId()));
        blocks.add(new MemoryPoolState(post82Mempool, "post-b82 tx resurrection"));

        // Test invalid opcodes in dead execution paths.
        // -> b81 (26) -> b82 (27) -> b83 (28)
        // b83 creates a tx which contains a transaction script with an invalid opcode in a dead execution path:
        // OP_FALSE OP_IF OP_INVALIDOPCODE OP_ELSE OP_TRUE OP_ENDIF
        //
        TransactionOutPointWithValue out28 = spendableOutputs.poll(); checkState(out28 != null);

        NewBlock b83 = createNextBlock(b82, chainHeadHeight + 29, null, null);
        {
            Transaction tx1 = new Transaction();
            tx1.addOutput(new TransactionOutput(tx1, out28.value,
                    new byte[]{OP_IF, (byte) OP_INVALIDOPCODE, OP_ELSE, OP_TRUE, OP_ENDIF}));
            addOnlyInputToTransaction(tx1, out28, 0);
            b83.addTransaction(tx1);
            Transaction tx2 = new Transaction();
            tx2.addOutput(new TransactionOutput(tx2, ZERO, new byte[]{OP_TRUE}));
            tx2.addInput(new TransactionInput(tx2, new byte[] { OP_FALSE },
                    new TransactionOutPoint(0, tx1.getTxId())));
            b83.addTransaction(tx2);
        }
        b83.solve();
        blocks.add(new BlockAndValidity(b83, true, false, b83.getHash(), chainHeadHeight + 29, "b83"));
        spendableOutputs.offer(b83.getCoinbaseOutput());

        // Reorg on/off blocks that have OP_RETURN in them (and try to spend them)
        // -> b81 (26) -> b82 (27) -> b83 (28) -> b84 (29) -> b87 (30) -> b88 (31)
        //                                    \-> b85 (29) -> b86 (30)            \-> b89 (32)
        //
        TransactionOutPointWithValue out29 = spendableOutputs.poll(); checkState(out29 != null);
        TransactionOutPointWithValue out30 = spendableOutputs.poll(); checkState(out30 != null);
        TransactionOutPointWithValue out31 = spendableOutputs.poll(); checkState(out31 != null);
        TransactionOutPointWithValue out32 = spendableOutputs.poll(); checkState(out32 != null);

        NewBlock b84 = createNextBlock(b83, chainHeadHeight + 30, out29, null);
        Transaction b84tx1 = new Transaction();
        {
            b84tx1.addOutput(new TransactionOutput(b84tx1, ZERO, new byte[]{OP_RETURN}));
            b84tx1.addOutput(new TransactionOutput(b84tx1, ZERO, new byte[]{OP_TRUE}));
            b84tx1.addOutput(new TransactionOutput(b84tx1, ZERO, new byte[]{OP_TRUE}));
            b84tx1.addOutput(new TransactionOutput(b84tx1, ZERO, new byte[]{OP_TRUE}));
            b84tx1.addOutput(new TransactionOutput(b84tx1, ZERO, new byte[]{OP_TRUE}));
            addOnlyInputToTransaction(b84tx1, b84);
            b84.addTransaction(b84tx1);

            Transaction tx2 = new Transaction();
            tx2.addOutput(new TransactionOutput(tx2, ZERO, new byte[]{OP_RETURN}));
            tx2.addOutput(new TransactionOutput(tx2, ZERO, new byte[]{OP_RETURN}));
            tx2.addInput(new TransactionInput(tx2, new byte[]{OP_TRUE}, new TransactionOutPoint(1, b84tx1)));
            b84.addTransaction(tx2);

            Transaction tx3 = new Transaction();
            tx3.addOutput(new TransactionOutput(tx3, ZERO, new byte[]{OP_RETURN}));
            tx3.addOutput(new TransactionOutput(tx3, ZERO, new byte[]{OP_TRUE}));
            tx3.addInput(new TransactionInput(tx3, new byte[]{OP_TRUE}, new TransactionOutPoint(2, b84tx1)));
            b84.addTransaction(tx3);

            Transaction tx4 = new Transaction();
            tx4.addOutput(new TransactionOutput(tx4, ZERO, new byte[]{OP_TRUE}));
            tx4.addOutput(new TransactionOutput(tx4, ZERO, new byte[]{OP_RETURN}));
            tx4.addInput(new TransactionInput(tx4, new byte[]{OP_TRUE}, new TransactionOutPoint(3, b84tx1)));
            b84.addTransaction(tx4);

            Transaction tx5 = new Transaction();
            tx5.addOutput(new TransactionOutput(tx5, ZERO, new byte[]{OP_RETURN}));
            tx5.addInput(new TransactionInput(tx5, new byte[]{OP_TRUE}, new TransactionOutPoint(4, b84tx1)));
            b84.addTransaction(tx5);
        }
        b84.solve();
        blocks.add(new BlockAndValidity(b84, true, false, b84.getHash(), chainHeadHeight + 30, "b84"));
        spendableOutputs.offer(b84.getCoinbaseOutput());

        NewBlock b85 = createNextBlock(b83, chainHeadHeight + 30, out29, null);
        blocks.add(new BlockAndValidity(b85, true, false, b84.getHash(), chainHeadHeight + 30, "b85"));

        NewBlock b86 = createNextBlock(b85, chainHeadHeight + 31, out30, null);
        blocks.add(new BlockAndValidity(b86, true, false, b86.getHash(), chainHeadHeight + 31, "b86"));

        NewBlock b87 = createNextBlock(b84, chainHeadHeight + 31, out30, null);
        blocks.add(new BlockAndValidity(b87, true, false, b86.getHash(), chainHeadHeight + 31, "b87"));
        spendableOutputs.offer(b87.getCoinbaseOutput());

        NewBlock b88 = createNextBlock(b87, chainHeadHeight + 32, out31, null);
        blocks.add(new BlockAndValidity(b88, true, false, b88.getHash(), chainHeadHeight + 32, "b88"));
        spendableOutputs.offer(b88.getCoinbaseOutput());

        NewBlock b89 = createNextBlock(b88, chainHeadHeight + 33, out32, null);
        {
            Transaction tx = new Transaction();
            tx.addOutput(new TransactionOutput(tx, ZERO, new byte[] {OP_TRUE}));
            tx.addInput(new TransactionInput(tx, new byte[]{OP_TRUE}, new TransactionOutPoint(0, b84tx1)));
            b89.addTransaction(tx);
            b89.solve();
        }
        blocks.add(new BlockAndValidity(b89, false, true, b88.getHash(), chainHeadHeight + 32, "b89"));

        // The remaining tests arent designed to fit in the standard flow, and thus must always come last
        // Add new tests here.

        //TODO: Explicitly address MoneyRange() checks

        if (!runBarelyExpensiveTests) {
            if (outStream != null)
                outStream.close();

            // (finally) return the created chain
            return ret;
        }

        // Test massive reorgs (in terms of block count/size)
        // -> b81 (26) -> b82 (27) -> b83 (28) -> b84 (29) -> b87 (30) -> b88 (31) -> lots of blocks -> b1000
        //                                    \-> b85 (29) -> b86 (30)            \-> lots more blocks
        //
        NewBlock largeReorgFinal;
        int LARGE_REORG_SIZE = 1008; // +/- a week of blocks
        int largeReorgLastHeight = chainHeadHeight + 33 + LARGE_REORG_SIZE + 1;
        {
            NewBlock nextBlock = b88;
            int nextHeight = chainHeadHeight + 33;
            TransactionOutPointWithValue largeReorgOutput = out32;
            for (int i = 0; i < LARGE_REORG_SIZE; i++) {
                nextBlock = createNextBlock(nextBlock, nextHeight, largeReorgOutput, null);
                Transaction tx = new Transaction();
                byte[] outputScript = new byte[Block.MAX_BLOCK_SIZE - nextBlock.block.messageSize() - 65];
                Arrays.fill(outputScript, (byte) OP_FALSE);
                tx.addOutput(new TransactionOutput(tx, ZERO, outputScript));
                addOnlyInputToTransaction(tx, nextBlock);
                nextBlock.addTransaction(tx);
                nextBlock.solve();
                blocks.add(new BlockAndValidity(nextBlock, true, false, nextBlock.getHash(), nextHeight++, "large reorg initial blocks " + i));
                spendableOutputs.offer(nextBlock.getCoinbaseOutput());
                largeReorgOutput = spendableOutputs.poll();
            }
            NewBlock reorgBase = b88;
            int reorgBaseHeight = chainHeadHeight + 33;
            for (int i = 0; i < LARGE_REORG_SIZE; i++) {
                reorgBase = createNextBlock(reorgBase, reorgBaseHeight++, null, null);
                blocks.add(new BlockAndValidity(reorgBase, true, false, nextBlock.getHash(), nextHeight - 1, "large reorg reorg block " + i));
            }
            reorgBase = createNextBlock(reorgBase, reorgBaseHeight, null, null);
            blocks.add(new BlockAndValidity(reorgBase, true, false, reorgBase.getHash(), reorgBaseHeight, "large reorg reorging block"));
            nextBlock = createNextBlock(nextBlock, nextHeight, null, null);
            blocks.add(new BlockAndValidity(nextBlock, true, false, reorgBase.getHash(), nextHeight++, "large reorg second reorg initial"));
            spendableOutputs.offer(nextBlock.getCoinbaseOutput());
            nextBlock = createNextBlock(nextBlock, nextHeight, null, null); spendableOutputs.poll();
            blocks.add(new BlockAndValidity(nextBlock, true, false, nextBlock.getHash(), nextHeight++, "large reorg second reorg"));
            spendableOutputs.offer(nextBlock.getCoinbaseOutput());
            largeReorgFinal = nextBlock;
        }
        ret.maximumReorgBlockCount = Math.max(ret.maximumReorgBlockCount, LARGE_REORG_SIZE + 2);

        // Test massive reorgs (in terms of tx count)
        // -> b60 (17) -> b64 (18) -> b65 (19) -> b69 (20) -> b72 (21) -> b1001 (22) -> lots of outputs -> lots of spends
        // Reorg back to:
        // -> b60 (17) -> b64 (18) -> b65 (19) -> b69 (20) -> b72 (21) -> b1001 (22) -> empty blocks
        //
        NewBlock b1001 = createNextBlock(largeReorgFinal, largeReorgLastHeight + 1, spendableOutputs.poll(), null);
        blocks.add(new BlockAndValidity(b1001, true, false, b1001.getHash(), largeReorgLastHeight + 1, "b1001"));
        spendableOutputs.offer(b1001.getCoinbaseOutput());
        int heightAfter1001 = largeReorgLastHeight + 2;

        if (runExpensiveTests) {
            // No way you can fit this test in memory
            checkArgument(blockStorageFile != null);

            NewBlock lastBlock = b1001;
            TransactionOutPoint lastOutput = new TransactionOutPoint(1, b1001.block.getTransactions().get(1).getTxId());
            int blockCountAfter1001;
            int nextHeight = heightAfter1001;

            List<Sha256Hash> hashesToSpend = new LinkedList<>(); // all index 0
            final int TRANSACTION_CREATION_BLOCKS = 100;
            for (blockCountAfter1001 = 0; blockCountAfter1001 < TRANSACTION_CREATION_BLOCKS; blockCountAfter1001++) {
                NewBlock block = createNextBlock(lastBlock, nextHeight++, null, null);
                while (block.block.messageSize() < Block.MAX_BLOCK_SIZE - 500) {
                    Transaction tx = new Transaction();
                    tx.addInput(lastOutput.hash(), lastOutput.index(), OP_NOP_SCRIPT);
                    tx.addOutput(ZERO, OP_TRUE_SCRIPT);
                    tx.addOutput(ZERO, OP_TRUE_SCRIPT);
                    lastOutput = new TransactionOutPoint(1, tx.getTxId());
                    hashesToSpend.add(tx.getTxId());
                    block.addTransaction(tx);
                }
                block.solve();
                blocks.add(new BlockAndValidity(block, true, false, block.getHash(), nextHeight-1,
                        "post-b1001 repeated transaction generator " + blockCountAfter1001 + "/" + TRANSACTION_CREATION_BLOCKS).setSendOnce(true));
                lastBlock = block;
            }

            Iterator<Sha256Hash> hashes = hashesToSpend.iterator();
            for (int i = 0; hashes.hasNext(); i++) {
                NewBlock block = createNextBlock(lastBlock, nextHeight++, null, null);
                while (block.block.messageSize() < Block.MAX_BLOCK_SIZE - 500 && hashes.hasNext()) {
                    Transaction tx = new Transaction();
                    tx.addInput(hashes.next(), 0, OP_NOP_SCRIPT);
                    tx.addOutput(ZERO, OP_TRUE_SCRIPT);
                    block.addTransaction(tx);
                }
                block.solve();
                blocks.add(new BlockAndValidity(block, true, false, block.getHash(), nextHeight-1,
                        "post-b1001 repeated transaction spender " + i).setSendOnce(true));
                lastBlock = block;
                blockCountAfter1001++;
            }

            // Reorg back to b1001 + empty blocks
            Sha256Hash firstHash = lastBlock.getHash();
            int height = nextHeight-1;
            nextHeight = heightAfter1001;
            lastBlock = b1001;
            for (int i = 0; i < blockCountAfter1001; i++) {
                NewBlock block = createNextBlock(lastBlock, nextHeight++, null, null);
                blocks.add(new BlockAndValidity(block, true, false, firstHash, height, "post-b1001 empty reorg block " + i + "/" + blockCountAfter1001));
                lastBlock = block;
            }

            // Try to spend from the other chain
            NewBlock b1002 = createNextBlock(lastBlock, nextHeight, null, null);
            {
                Transaction tx = new Transaction();
                tx.addInput(hashesToSpend.get(0), 0, OP_NOP_SCRIPT);
                tx.addOutput(ZERO, OP_TRUE_SCRIPT);
                b1002.addTransaction(tx);
            }
            b1002.solve();
            blocks.add(new BlockAndValidity(b1002, false, true, firstHash, height, "b1002"));

            // Now actually reorg
            NewBlock b1003 = createNextBlock(lastBlock, nextHeight, null, null);
            blocks.add(new BlockAndValidity(b1003, true, false, b1003.getHash(), nextHeight, "b1003"));

            // Now try to spend again
            NewBlock b1004 = createNextBlock(b1003, nextHeight + 1, null, null);
            {
                Transaction tx = new Transaction();
                tx.addInput(hashesToSpend.get(0), 0, OP_NOP_SCRIPT);
                tx.addOutput(ZERO, OP_TRUE_SCRIPT);
                b1004.addTransaction(tx);
            }
            b1004.solve();
            blocks.add(new BlockAndValidity(b1004, false, true, b1003.getHash(), nextHeight, "b1004"));

            ret.maximumReorgBlockCount = Math.max(ret.maximumReorgBlockCount, blockCountAfter1001);
        }

        if (outStream != null)
            outStream.close();

        // (finally) return the created chain
        return ret;
    }

    private byte uniquenessCounter = 0;
    private NewBlock createNextBlock(Block baseBlock, int nextBlockHeight, @Nullable TransactionOutPointWithValue prevOut,
                                     Coin additionalCoinbaseValue) throws ScriptException {
        Integer height = blockToHeightMap.get(baseBlock.getHash());
        if (height != null)
            checkState(height == nextBlockHeight - 1);
        Coin coinbaseValue = FIFTY_COINS.shiftRight(nextBlockHeight / params.getSubsidyDecreaseBlockCount())
                .add((prevOut != null ? prevOut.value.subtract(SATOSHI) : ZERO))
                .add(additionalCoinbaseValue == null ? ZERO : additionalCoinbaseValue);
        Block block = baseBlock.createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, coinbaseOutKeyPubKey, coinbaseValue, nextBlockHeight);
        Transaction t = new Transaction();
        if (prevOut != null) {
            // Entirely invalid scriptPubKey to ensure we aren't pre-verifying too much
            t.addOutput(new TransactionOutput(t, ZERO, new byte[] {(byte)(new Random().nextInt() & 0xff), uniquenessCounter++}));
            // Spendable output
            t.addOutput(new TransactionOutput(t, SATOSHI, new byte[] {OP_1}));
            addOnlyInputToTransaction(t, prevOut);
            block.addTransaction(t);
            block.solve();
        }
        return new NewBlock(block, prevOut == null ? null : new TransactionOutPointWithValue(t, 1));
    }
    private NewBlock createNextBlock(NewBlock baseBlock, int nextBlockHeight, @Nullable TransactionOutPointWithValue prevOut,
                                     Coin additionalCoinbaseValue) throws ScriptException {
        return createNextBlock(baseBlock.block, nextBlockHeight, prevOut, additionalCoinbaseValue);
    }

    private void addOnlyInputToTransaction(Transaction t, NewBlock block) throws ScriptException {
        addOnlyInputToTransaction(t, block.getSpendableOutput(), TransactionInput.NO_SEQUENCE);
    }

    private void addOnlyInputToTransaction(Transaction t, TransactionOutPointWithValue prevOut) throws ScriptException {
        addOnlyInputToTransaction(t, prevOut, TransactionInput.NO_SEQUENCE);
    }

    private void addOnlyInputToTransaction(Transaction t, TransactionOutPointWithValue prevOut, long sequence) throws ScriptException {
        TransactionInput input = new TransactionInput(t, new byte[]{}, prevOut.outpoint);
        input.setSequenceNumber(sequence);
        t.addInput(input);

        if (prevOut.scriptPubKey.chunks().get(0).equalsOpCode(OP_TRUE)) {
            input.setScriptSig(new ScriptBuilder().op(OP_1).build());
        } else {
            // Sign input
            checkState(ScriptPattern.isP2PK(prevOut.scriptPubKey));
            Sha256Hash hash = t.hashForSignature(0, prevOut.scriptPubKey, SigHash.ALL, false);
            input.setScriptSig(ScriptBuilder.createInputScript(
                            new TransactionSignature(coinbaseOutKey.sign(hash), SigHash.ALL, false))
            );
        }
    }

    /**
     * Represents a block which is sent to the tested application and which the application must either reject or accept,
     * depending on the flags in the rule
     */
    class BlockAndValidity extends Rule {
        Block block;
        Sha256Hash blockHash;
        boolean connects;
        boolean throwsException;
        boolean sendOnce; // We can throw away the memory for this block once we send it the first time (if bitcoind asks again, its broken)
        Sha256Hash hashChainTipAfterBlock;
        int heightAfterBlock;

        public BlockAndValidity(Block block, boolean connects, boolean throwsException, Sha256Hash hashChainTipAfterBlock, int heightAfterBlock, String blockName) {
            super(blockName);
            if (connects && throwsException)
                throw new RuntimeException("A block cannot connect if an exception was thrown while adding it.");
            this.block = block;
            this.blockHash = block.getHash();
            this.connects = connects;
            this.throwsException = throwsException;
            this.hashChainTipAfterBlock = hashChainTipAfterBlock;
            this.heightAfterBlock = heightAfterBlock;

            // Keep track of the set of blocks indexed by hash
            hashHeaderMap.put(block.getHash(), block.cloneAsHeader());

            // Double-check that we are always marking any given block at the same height
            Integer height = blockToHeightMap.get(hashChainTipAfterBlock);
            if (height != null)
                checkState(height == heightAfterBlock);
            else
                blockToHeightMap.put(hashChainTipAfterBlock, heightAfterBlock);
        }

        public BlockAndValidity(NewBlock block, boolean connects, boolean throwsException, Sha256Hash hashChainTipAfterBlock, int heightAfterBlock, String blockName) {
            this(block.block, connects, throwsException, hashChainTipAfterBlock, heightAfterBlock, blockName);
            coinbaseBlockMap.put(block.getCoinbaseOutput().outpoint.hash(), block.getHash());
            Integer blockHeight = blockToHeightMap.get(block.block.getPrevBlockHash());
            if (blockHeight != null) {
                blockHeight++;
                for (Transaction t : block.block.getTransactions())
                    for (TransactionInput in : t.getInputs()) {
                        Sha256Hash blockSpendingHash = coinbaseBlockMap.get(in.getOutpoint().hash());
                        checkState(blockSpendingHash == null || blockToHeightMap.get(blockSpendingHash) == null ||
                                blockToHeightMap.get(blockSpendingHash) == blockHeight - params.getSpendableCoinbaseDepth());
                    }
            }
        }

        public BlockAndValidity setSendOnce(boolean sendOnce) {
            this.sendOnce = sendOnce;
            return this;
        }
    }
}
