#!/usr/bin/env python3
# Copyright (c) 2021-2022 The Bitcoin developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""
This tests the new May 2022 upgrade8 feature: native introspection
opcodes.
See: https://gitlab.com/GeneralProtocols/research/chips/-/blob/master/CHIP-2021-02-Add-Native-Introspection-Opcodes.md
"""

import os
from typing import List, Set, Tuple, Union

from test_framework.address import (
    script_to_p2sh, hash160
)
from test_framework.blocktools import (
    create_block,
    create_coinbase,
    create_tx_with_script,
    make_conform_to_ctor,
)
from test_framework.key import ECKey
from test_framework.messages import (
    CBlock,
    COutPoint,
    CTransaction,
    CTxIn,
    CTxOut,
    FromHex,
    ser_uint256, uint256_from_str
)
from test_framework.p2p import (
    P2PDataStore,
)
from test_framework import schnorr
from test_framework.script import (
    CScript,
    CScriptNum,
    OP_1NEGATE, OP_0, OP_1, OP_2, OP_3, OP_4, OP_5, OP_7, OP_10,
    OP_CHECKMULTISIG, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160,
    OP_DEPTH, OP_IF, OP_ENDIF,
    OP_ADD,
    OP_TRUE,
    OP_DUP, OP_CAT, OP_DROP, OP_SWAP, OP_SIZE, OP_ROLL,
    OP_CODESEPARATOR, OP_RETURN,
    # Native Introspection - Nullary
    OP_INPUTINDEX, OP_ACTIVEBYTECODE, OP_TXVERSION, OP_TXINPUTCOUNT, OP_TXOUTPUTCOUNT, OP_TXLOCKTIME,
    # Native Introspection - Unary
    OP_UTXOVALUE, OP_UTXOBYTECODE, OP_OUTPOINTTXHASH, OP_OUTPOINTINDEX, OP_INPUTBYTECODE, OP_INPUTSEQUENCENUMBER,
    OP_OUTPUTVALUE, OP_OUTPUTBYTECODE,
    SIGHASH_ALL,
    SIGHASH_FORKID,
    SignatureHashForkId,
)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal

# Errors generated by vm when specifying an invalid input/output index for ops such as: OP_INPUT*, OP_OUTPUT*, etc
INPUT_INDEX_OUT_OF_RANGE_ERROR = ("mandatory-script-verify-flag-failed"
                                  " (The specified transaction input index is out of range)")
OUTPUT_INDEX_OUT_OF_RANGE_ERROR = ("mandatory-script-verify-flag-failed"
                                   " (The specified transaction output index is out of range)")
# This test assumes a maximum of 4-byte CScriptNums since the int64 upgrade is separate from this.
CSCRIPTNUM_MAX_INT = 2 ** 31 - 1


class NativeIntrospectionTest(BitcoinTestFramework):
    # A "permissive" scriptPubKey that drops the top 2 items from the stack (if any), so we can embed arbitrary data
    # into scriptSigs
    scriptPubKey_stack_dropper = CScript([OP_DEPTH, OP_IF, OP_DROP, OP_DEPTH, OP_IF, OP_DROP, OP_ENDIF, OP_ENDIF,
                                          OP_TRUE])

    def set_test_params(self):
        self.num_nodes = 1
        self.block_heights = {}
        self.extra_args = [
            # Node0 has native introspection activated (activates at upgrade8)
            ["-acceptnonstdtxn=1", "-expire=0"],
        ]

    def bootstrap_p2p(self, *, num_connections=1):
        """Add a P2P connection to the node.

        Helper to connect and wait for version handshake."""
        for _ in range(num_connections):
            self.nodes[0].add_p2p_connection(P2PDataStore())
        for p2p in self.nodes[0].p2ps:
            p2p.wait_for_getheaders()

    def reconnect_p2p(self, **kwargs):
        """Tear down and bootstrap the P2P connection to the node.

        The node gets disconnected several times in this test. This helper
        method reconnects the p2p and restarts the network thread."""
        self.nodes[0].disconnect_p2ps()
        self.bootstrap_p2p(**kwargs)

    def getbestblock(self, node):
        """Get the best block. Register its height so we can use build_block."""
        block_height = node.getblockcount()
        blockhash = node.getblockhash(block_height)
        block = FromHex(CBlock(), node.getblock(blockhash, 0))
        block.calc_sha256()
        self.block_heights[block.sha256] = block_height
        return block

    def build_block(self, parent, transactions=(), nTime=None):
        """Make a new block with an OP_1 coinbase output.

        Requires parent to have its height registered."""
        parent.calc_sha256()
        block_height = self.block_heights[parent.sha256] + 1
        block_time = (parent.nTime + 1) if nTime is None else nTime

        coinbase = create_coinbase(block_height)
        # prepend some code to coinbase scriptPubKey to drop up to 2 items from the stack
        coinbase.vout[0].scriptPubKey = self.scriptPubKey_stack_dropper
        # Modify coinbase to make the reward be below 2.1 billion satoshis so that OP_OUTPUTVALUE
        # is guaranteed to work even with 4-byte CScriptNums.
        diff = coinbase.vout[0].nValue - CSCRIPTNUM_MAX_INT
        if diff > 0:
            coinbase.vout[0].nValue = CSCRIPTNUM_MAX_INT
            coinbase.vout.append(CTxOut(diff, CScript([OP_TRUE])))
        coinbase.calc_sha256()
        block = create_block(parent.sha256, coinbase, block_time)
        block.vtx.extend(transactions)
        make_conform_to_ctor(block)
        block.hashMerkleRoot = block.calc_merkle_root()
        block.solve()
        self.block_heights[block.sha256] = block_height
        return block

    def ensure_spendable_txns_have_differing_amounts(self, node, scriptPubKey,
                                                     spendable_txns: List[CTransaction]) -> List[CTransaction]:
        """Takes an array of spendable_txns and spends them all with differing amounts, then mines
        a block.  It then returns the confirmed txns generated from this operation.

        This step helps to ensure all coins have unique amounts, which helps this test check that the
        UTXO-value related introspection ops are sane.

        Note that param `node` must have a valid p2p connection at `node.p2p`."""
        ret = []
        values = []
        delta = 0
        for spend_tx in spendable_txns:
            # Spend each output in spend_tx as a separate txn
            for out_num in range(len(spend_tx.vout)):
                delta += 1
                output = spend_tx.vout[out_num]
                value = output.nValue - 500 - delta  # Include unique fee to ensure unique output coin value
                if value < 576:
                    # skip dust, OP_RETURNS, etc, if any
                    continue
                tx = create_tx_with_script(spend_tx, out_num, b'', value, scriptPubKey)
                values.append(value)
                ret.append(tx)
        # Paranoia: Ensure we got at least *as many* coins out of this as there were input txns
        assert len(ret) >= len(spendable_txns)
        # Ensure all amounts are unique
        assert_equal(len(set(values)), len(values))
        # Send txs to mempool
        node.p2p.send_txs_and_test(ret, node)
        assert_equal(node.getrawmempool(), [tx.hash for tx in ret])
        # Mine the block
        tiphash = self.generatetoaddress(node, 1, node.get_deterministic_priv_key().address)[0]
        # Ensure mempool empty
        assert_equal(node.getrawmempool(), [])
        block = node.getblock(tiphash, 1)
        # Ensure all txns made it into block
        assert all(tx.hash in block['tx'] for tx in ret)

        return ret

    def run_test(self):
        node = self.nodes[0]

        self.bootstrap_p2p()

        self.log.info("Create some blocks with OP_1 coinbase for spending.")
        tip = self.getbestblock(node)
        blocks = []
        for _ in range(200):  # We'll need about 200 coins for this test
            tip = self.build_block(tip)
            blocks.append(tip)
        node.p2p.send_blocks_and_test(blocks, node, success=True)
        spendable_txns = [block.vtx[0] for block in blocks]

        self.log.info("Mature the blocks and get out of IBD.")
        self.generatetoaddress(node, 100, node.get_deterministic_priv_key().address)

        self.log.info("Setting up spends to test and mining the funding txns")

        # Next, generate a txn set that has all unique amounts for the coins, as an extra step to ensure
        # each coin is as different as possible, in order to be able to best validate native introspection.
        spendable_txns = self.ensure_spendable_txns_have_differing_amounts(
            node, self.scriptPubKey_stack_dropper, spendable_txns)

        # Generate a key pair
        privkeybytes = os.urandom(32)
        private_key = ECKey()
        private_key.set(privkeybytes, True)
        # get uncompressed public key serialization
        public_key = private_key.get_pubkey().get_bytes()

        def calc_value_minus_fee(value, nOuts=1):
            assert nOuts > 0
            return value - 500 - ((nOuts - 1) * 38)

        def create_fund_and_spend_tx(scriptsigextra, redeemextra,
                                     *, nIns=1, nOuts=1, nSpendOuts=1, nVersion=None, nLockTime=None, multisig=True,
                                     pushInputHash=False, getTxOuts=None, sequenceNums=None,
                                     pushOutputValues=False,
                                     pushOutputScriptPubKeys=False) -> Union[Tuple[CTransaction, CTransaction],
                                                                             Tuple[CTransaction, CTransaction,
                                                                                   List[CTxOut]]]:
            assert 1 <= nIns <= len(spendable_txns)
            assert 1 <= nOuts <= 64  # Limit the number of random dusts to prevent running out of money for final txn
            assert 1 <= nSpendOuts <= 64
            txOuts = []
            spendfrom = spendable_txns.pop()

            if sequenceNums is None:
                sequenceNums = [0 for _ in range(nIns)]
            assert len(sequenceNums) == nIns

            if multisig:
                redeemScript = CScript(redeemextra + [OP_1, public_key, OP_1, OP_CHECKMULTISIG])
            else:
                assert len(redeemextra) > 0
                redeemScript = CScript(redeemextra)
            scriptPubKey = CScript([OP_HASH160, hash160(redeemScript), OP_EQUAL])

            value = spendfrom.vout[0].nValue
            value1 = calc_value_minus_fee(value)

            # Fund transaction
            txfund = create_tx_with_script(spendfrom, 0, b'', value1, scriptPubKey)
            inputIndex = 0

            while nSpendOuts > 1:
                # Fill the funding tx with OP_RETURN outputs, to give the real output a higher N index
                txfund.vout.insert(inputIndex, CTxOut(0, CScript([OP_RETURN, CScriptNum(inputIndex)])))
                inputIndex += 1
                nSpendOuts -= 1

            txfund.rehash()

            p2sh = script_to_p2sh(redeemScript)
            self.log.info(f"scriptPubKey {scriptPubKey!r}")
            self.log.info(f"redeemScript {redeemScript!r} -> p2sh address {p2sh}")

            # Spend transaction
            value2 = calc_value_minus_fee(value1, nOuts=nOuts)
            txspend = CTransaction()
            if nVersion is not None:
                txspend.nVersion = nVersion
            if nLockTime is not None:
                txspend.nLockTime = nLockTime

            # add additional inputs before the special script-ey input
            while nIns > 1:
                tx_extra = spendable_txns.pop()
                # The extra inputs contain a scriptSig that is a CScriptNum that is 100 + inputNum
                inpNum = len(txspend.vin)
                nSequence = sequenceNums[inpNum]
                txspend.vin.append(CTxIn(COutPoint(tx_extra.sha256, 0), CScript([CScriptNum(100 + inpNum)]),
                                         nSequence))
                value2 += tx_extra.vout[0].nValue - 50
                txOuts.append(tx_extra.vout[0])
                nIns -= 1

            txspend.vout.append(CTxOut(value2, self.scriptPubKey_stack_dropper))
            nOuts -= 1

            # Add additional outputs as: random_small_dust, "OP_RETURN OutNum"
            amts: Set[int] = set()
            while nOuts > 0:
                outNum = len(txspend.vout)
                amt = 0
                while amt < 576 or amt in amts:
                    amt = int.from_bytes(os.urandom(2), byteorder="little")
                # we "burn" some small dust to mark each OP_RETURN's value uniquely for OP_OUTPUTVALUE
                txspend.vout.append(CTxOut(amt, CScript([OP_RETURN, CScriptNum(outNum)])))
                value2 -= amt
                nOuts -= 1
            txspend.vout[0].nValue = value2  # ensure if we added burn outputs, that the real output value is correct

            txspend.vin.append(CTxIn(COutPoint(txfund.sha256, inputIndex), b'', sequenceNums[-1]))
            nIns -= 1
            assert nIns == 0
            if pushInputHash:
                scriptsigextra.insert(0, ser_uint256(txfund.sha256))
            if pushOutputValues:
                for out in txspend.vout:
                    scriptsigextra.append(out.nValue)
            if pushOutputScriptPubKeys:
                for out in txspend.vout:
                    scriptsigextra.append(out.scriptPubKey)
            txOuts.append(txfund.vout[0])
            scriptey_input_index = len(txspend.vin) - 1

            if multisig:
                # Sign the transaction
                sighashtype = SIGHASH_ALL | SIGHASH_FORKID
                hashbyte = bytes([sighashtype & 0xff])
                sighash = SignatureHashForkId(
                    redeemScript, txspend, scriptey_input_index, sighashtype, value1)
                txsig = schnorr.sign(privkeybytes, sighash) + hashbyte
                dummy = OP_1  # Required for 1-of-1 schnorr sig
                txspend.vin[scriptey_input_index].scriptSig = ss = CScript([dummy, txsig] + scriptsigextra
                                                                           + [redeemScript])
            else:
                txspend.vin[scriptey_input_index].scriptSig = ss = CScript(scriptsigextra + [redeemScript])

            self.log.info(f"scriptSig: {ss!r}")
            txspend.rehash()

            if getTxOuts:
                return txfund, txspend, txOuts
            return txfund, txspend

        mempool = []

        # --------------------------------------------------------------------
        # Nullary (0-arg) introspection ops
        # --------------------------------------------------------------------

        # OP_INTPUTINDEX (script input is input 0)
        ssextra = [OP_0]
        rsextra = [OP_INPUTINDEX, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_INTPUTINDEX (script input is input 2)
        ssextra = [OP_2]
        rsextra = [OP_INPUTINDEX, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_ACTIVEBYTECODE (no code separator)
        ssextra = [CScript([OP_ADD, OP_5, OP_EQUALVERIFY, OP_ACTIVEBYTECODE, OP_EQUALVERIFY, OP_TRUE]), OP_2, OP_3]
        rsextra = [OP_ADD, OP_5, OP_EQUALVERIFY, OP_ACTIVEBYTECODE, OP_EQUALVERIFY, OP_TRUE]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3, multisig=False)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_ACTIVEBYTECODE (with code separator)
        ssextra = [CScript([OP_EQUALVERIFY, OP_ACTIVEBYTECODE, OP_EQUALVERIFY, OP_TRUE]), OP_2, OP_3]
        rsextra = [OP_ADD, OP_5, OP_CODESEPARATOR, OP_EQUALVERIFY, OP_ACTIVEBYTECODE, OP_EQUALVERIFY, OP_TRUE]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3, multisig=False)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_ACTIVEBYTECODE (multiple code separators)
        ssextra = [CScript([OP_ACTIVEBYTECODE, OP_EQUALVERIFY, OP_TRUE]),
                   CScript([OP_EQUALVERIFY, OP_ACTIVEBYTECODE, OP_EQUALVERIFY,
                            OP_CODESEPARATOR, OP_ACTIVEBYTECODE, OP_EQUALVERIFY, OP_TRUE]), OP_2, OP_3]
        rsextra = [OP_ADD, OP_5, OP_CODESEPARATOR, OP_EQUALVERIFY, OP_ACTIVEBYTECODE, OP_EQUALVERIFY,
                   OP_CODESEPARATOR, OP_ACTIVEBYTECODE, OP_EQUALVERIFY,
                   OP_TRUE]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3, multisig=False)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_TXVERSION (version == 1)
        ssextra = [OP_1]
        rsextra = [OP_TXVERSION, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nVersion=1)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_TXVERSION (version == 2)
        ssextra = [OP_2]
        rsextra = [OP_TXVERSION, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nVersion=2)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_TXINPUTCOUNT (1)
        ssextra = [OP_1]
        rsextra = [OP_TXINPUTCOUNT, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=1)
        assert_equal(len(tx.vin), 1)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_TXINPUTCOUNT (2)
        ssextra = [OP_2]
        rsextra = [OP_TXINPUTCOUNT, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=2)
        assert_equal(len(tx.vin), 2)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_TXINPUTCOUNT (10)
        ssextra = [OP_10]
        rsextra = [OP_TXINPUTCOUNT, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=10)
        assert_equal(len(tx.vin), 10)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_TXINPUTCOUNT (25)
        ssextra = [CScriptNum(25)]
        rsextra = [OP_TXINPUTCOUNT, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=25)
        assert_equal(len(tx.vin), 25)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_TXOUTPUTCOUNT (1)
        ssextra = [OP_1]
        rsextra = [OP_TXOUTPUTCOUNT, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nOuts=1)
        assert_equal(len(tx.vout), 1)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_TXOUTPUTCOUNT (2)
        ssextra = [OP_2]
        rsextra = [OP_TXOUTPUTCOUNT, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nOuts=2)
        assert_equal(len(tx.vout), 2)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_TXOUTPUTCOUNT (25)
        ssextra = [CScriptNum(25)]
        rsextra = [OP_TXOUTPUTCOUNT, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nOuts=25)
        assert_equal(len(tx.vout), 25)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_TXLOCKTIME (0)
        ssextra = [OP_0]
        rsextra = [OP_TXLOCKTIME, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nLockTime=0)
        assert_equal(tx.nLockTime, 0)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_TXLOCKTIME (10)
        ssextra = [OP_10]
        rsextra = [OP_TXLOCKTIME, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nLockTime=10)
        assert_equal(tx.nLockTime, 10)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_TXLOCKTIME (current_height - 10)
        current_height = node.getblockchaininfo()["blocks"]
        assert current_height > 10
        ssextra = [CScriptNum(current_height - 10)]
        rsextra = [OP_TXLOCKTIME, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nLockTime=current_height - 10)
        assert_equal(tx.nLockTime, current_height - 10)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # --------------------------------------------------------------------
        # Unary (1-arg) introspection ops
        # --------------------------------------------------------------------

        # OP_UTXOVALUE (1 input)
        # Here we have: spendable_txns[-1] -> txfund -> input0
        output = spendable_txns[-1].vout[0]
        inp_coin_value = calc_value_minus_fee(output.nValue)  # Calculate the value of the tx0 input coin
        assert inp_coin_value > 0
        ssextra = [CScriptNum(inp_coin_value)]
        rsextra = [OP_0, OP_UTXOVALUE, OP_EQUALVERIFY]
        tx0, tx, txOuts = create_fund_and_spend_tx(ssextra, rsextra, getTxOuts=True)
        assert_equal(txOuts[0].nValue, inp_coin_value)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_UTXOVALUE (3 inputs, check value of other input)
        # Here we have: spendable_txns[-2] -> input0, spendable_txns[-3] -> input1,
        #               spendable_txns[-1] -> txfund -> input2
        output = spendable_txns[-3].vout[0]
        inp_coin_value = output.nValue
        assert inp_coin_value > 0
        ssextra = [CScriptNum(inp_coin_value)]
        rsextra = [OP_1, OP_UTXOVALUE, OP_EQUALVERIFY]
        tx0, tx, txOuts = create_fund_and_spend_tx(ssextra, rsextra, getTxOuts=True, nIns=3)
        assert_equal(txOuts[1].nValue, inp_coin_value)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_UTXOVALUE (3 inputs, input 3, invalid index)
        ssextra = []
        rsextra = [OP_3, OP_UTXOVALUE, OP_DROP]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3)
        node.p2p.send_txs_and_test([tx0, tx], node, success=False, expect_disconnect=True,
                                   reject_reason=INPUT_INDEX_OUT_OF_RANGE_ERROR)
        mempool += [tx0.hash]
        assert_equal(node.getrawmempool(), mempool)
        self.reconnect_p2p()  # we lost the connection from above bad tx, reconnect

        # OP_UTXOVALUE (3 inputs, input -5, invalid negative index)
        ssextra = []
        rsextra = [CScriptNum(-5), OP_UTXOVALUE, OP_DROP]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3)
        node.p2p.send_txs_and_test([tx0, tx], node, success=False, expect_disconnect=True,
                                   reject_reason=INPUT_INDEX_OUT_OF_RANGE_ERROR)
        mempool += [tx0.hash]
        assert_equal(node.getrawmempool(), mempool)
        self.reconnect_p2p()  # we lost the connection from above bad tx, reconnect

        # OP_UTXOBYTECODE (2 inputs, second input)
        redeemScript = [OP_1, OP_UTXOBYTECODE, OP_EQUALVERIFY, OP_TRUE]
        scriptPubKey = CScript([OP_HASH160, hash160(CScript(redeemScript)), OP_EQUAL])
        ssextra = [scriptPubKey]
        tx0, tx, txOuts = create_fund_and_spend_tx(ssextra, redeemScript, getTxOuts=True, nIns=2, multisig=False)
        assert_equal(txOuts[1].scriptPubKey, scriptPubKey)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_UTXOBYTECODE (2 inputs, other input)
        # Here we have: spendable_txns[-2] -> input0, spendable_txns[-1] -> txfund -> input1
        output = spendable_txns[-2].vout[0]
        ssextra = [output.scriptPubKey]
        rsextra = [OP_0, OP_UTXOBYTECODE, OP_EQUALVERIFY]
        tx0, tx, txOuts = create_fund_and_spend_tx(ssextra, rsextra, getTxOuts=True, nIns=2)
        assert_equal(txOuts[0].scriptPubKey, output.scriptPubKey)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_UTXOBYTECODE (2 inputs, input 3, invalid index)
        ssextra = []
        rsextra = [OP_3, OP_UTXOBYTECODE, OP_DROP]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=2)
        node.p2p.send_txs_and_test([tx0, tx], node, success=False, expect_disconnect=True,
                                   reject_reason=INPUT_INDEX_OUT_OF_RANGE_ERROR)
        mempool += [tx0.hash]
        assert_equal(node.getrawmempool(), mempool)
        self.reconnect_p2p()  # we lost the connection from above bad tx, reconnect

        # OP_UTXOBYTECODE (2 inputs, input -1, invalid negative index)
        ssextra = []
        rsextra = [OP_1NEGATE, OP_UTXOBYTECODE, OP_DROP]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=2)
        node.p2p.send_txs_and_test([tx0, tx], node, success=False, expect_disconnect=True,
                                   reject_reason=INPUT_INDEX_OUT_OF_RANGE_ERROR)
        mempool += [tx0.hash]
        assert_equal(node.getrawmempool(), mempool)
        self.reconnect_p2p()  # we lost the connection from above bad tx, reconnect

        # OP_OUTPOINTTXHASH (3 inputs, input 0, other)
        # Here we have: spendable_txns[-2] -> input0, spendable_txns[-3] -> input1,
        #               spendable_txns[-1] -> txfund -> input2
        txid = spendable_txns[-2].sha256
        ssextra = [ser_uint256(txid)]
        rsextra = [OP_0, OP_OUTPOINTTXHASH, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3)
        assert_equal(tx.vin[0].prevout.hash, txid)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_OUTPOINTTXHASH (3 inputs, input 1, other)
        # Here we have: spendable_txns[-2] -> input0, spendable_txns[-3] -> input1,
        #               spendable_txns[-1] -> txfund -> input2
        txid = spendable_txns[-3].sha256
        ssextra = [ser_uint256(txid)]
        rsextra = [OP_1, OP_OUTPOINTTXHASH, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3)
        assert_equal(tx.vin[1].prevout.hash, txid)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_OUTPOINTTXHASH (3 inputs, input 2, self)
        ssextra = []  # Hash gets put here by pushInputHash=True below
        rsextra = [OP_2, OP_OUTPOINTTXHASH, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3, pushInputHash=True)
        assert_equal(tx.vin[2].prevout.hash, uint256_from_str(ssextra[0]))
        assert_equal(tx.vin[2].prevout.hash, tx0.sha256)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_OUTPOINTTXHASH (3 inputs, input 3, invalid index)
        ssextra = []
        rsextra = [OP_3, OP_OUTPOINTTXHASH, OP_DROP]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3)
        node.p2p.send_txs_and_test([tx0, tx], node, success=False, expect_disconnect=True,
                                   reject_reason=INPUT_INDEX_OUT_OF_RANGE_ERROR)
        mempool += [tx0.hash]
        assert_equal(node.getrawmempool(), mempool)
        self.reconnect_p2p()  # we lost the connection from above bad tx, reconnect

        # OP_OUTPOINTTXHASH (3 inputs, input -234, invalid negative index)
        ssextra = []
        rsextra = [CScriptNum(-234), OP_OUTPOINTTXHASH, OP_DROP]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3)
        node.p2p.send_txs_and_test([tx0, tx], node, success=False, expect_disconnect=True,
                                   reject_reason=INPUT_INDEX_OUT_OF_RANGE_ERROR)
        mempool += [tx0.hash]
        assert_equal(node.getrawmempool(), mempool)
        self.reconnect_p2p()  # we lost the connection from above bad tx, reconnect

        # OP_OUTPOINTINDEX (3 inputs, input 2, self)
        ssextra = [OP_4]  # We are prevout.GetN() == 4 due to nSpendOuts=5 below
        rsextra = [OP_2, OP_OUTPOINTINDEX, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3, nSpendOuts=5)
        assert_equal(tx.vin[2].prevout.n, 4)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_OUTPOINTINDEX (3 inputs, input 1)
        ssextra = [OP_0]
        rsextra = [OP_1, OP_OUTPOINTINDEX, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3, nSpendOuts=5)
        assert_equal(tx.vin[1].prevout.n, 0)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_OUTPOINTINDEX (3 inputs, input 3, invalid index)
        ssextra = []
        rsextra = [OP_3, OP_OUTPOINTINDEX, OP_DROP]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3)
        node.p2p.send_txs_and_test([tx0, tx], node, success=False, expect_disconnect=True,
                                   reject_reason=INPUT_INDEX_OUT_OF_RANGE_ERROR)
        mempool += [tx0.hash]
        assert_equal(node.getrawmempool(), mempool)
        self.reconnect_p2p()  # we lost the connection from above bad tx, reconnect

        # OP_OUTPOINTINDEX (3 inputs, input -4142133, invalid negative index)
        ssextra = []
        rsextra = [CScriptNum(-4142133), OP_OUTPOINTINDEX, OP_DROP]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3)
        node.p2p.send_txs_and_test([tx0, tx], node, success=False, expect_disconnect=True,
                                   reject_reason=INPUT_INDEX_OUT_OF_RANGE_ERROR)
        mempool += [tx0.hash]
        assert_equal(node.getrawmempool(), mempool)
        self.reconnect_p2p()  # we lost the connection from above bad tx, reconnect

        # OP_INPUTBYTECODE (3 inputs, test input 0 and input 1)
        ssextra = [CScript([CScriptNum(100)]), CScript([CScriptNum(101)])]
        rsextra = [OP_1, OP_INPUTBYTECODE, OP_EQUALVERIFY, OP_0, OP_INPUTBYTECODE, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3, nSpendOuts=5)
        assert_equal(tx.vin[0].scriptSig, CScript([CScriptNum(100)]))
        assert_equal(tx.vin[1].scriptSig, CScript([CScriptNum(101)]))
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_INPUTBYTECODE (3 inputs, test input 2, self)
        # This convoluted script reproduces the scriptSig on the stack, so that the scriptSig of this
        # input may be checked using OP_INPUTBYTECODE. This also tests that OP_CODESEPARATOR is a no-op
        # here and has no effect for OP_INPUTBYTECODE.
        script = [OP_DUP, OP_SIZE, OP_SWAP, OP_CAT, OP_1, OP_ROLL, OP_DROP, OP_DUP, OP_CAT,
                  OP_CODESEPARATOR, OP_2, OP_INPUTBYTECODE, OP_EQUALVERIFY, OP_TRUE]
        ssextra = [CScript(script)]
        rsextra = script
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3, nSpendOuts=5, multisig=False)
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_INPUTBYTECODE (3 inputs, test input 0 and input 7 -- invalid index)
        ssextra = [CScript([CScriptNum(107)]), CScript([CScriptNum(101)])]
        rsextra = [OP_1, OP_INPUTBYTECODE, OP_EQUALVERIFY, OP_7, OP_INPUTBYTECODE, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=3, nSpendOuts=5)
        assert_equal(tx.vin[0].scriptSig, CScript([CScriptNum(100)]))
        assert_equal(tx.vin[1].scriptSig, CScript([CScriptNum(101)]))
        node.p2p.send_txs_and_test([tx0, tx], node, success=False, expect_disconnect=True,
                                   reject_reason=INPUT_INDEX_OUT_OF_RANGE_ERROR)
        mempool += [tx0.hash]
        assert_equal(node.getrawmempool(), mempool)
        self.reconnect_p2p()  # we lost the connection from above bad tx, reconnect

        # OP_INPUTSEQUENCENUMBER (14 inputs, evaluate each input)
        sequenceNums = [16, 34_342, 0, 0, 1, 2, 3, 4, 5, 10, 15, 123_456_789, 65_536, 9999]
        nIns = len(sequenceNums)
        ssextra = [seq for seq in sequenceNums]
        rsextra = []
        for whichInp in range(nIns - 1, -1, -1):
            rsextra += [whichInp, OP_INPUTSEQUENCENUMBER, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=nIns, sequenceNums=sequenceNums)
        for whichInp in range(nIns):
            assert_equal(tx.vin[whichInp].nSequence, sequenceNums[whichInp])
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_INPUTSEQUENCENUMBER (3 inputs, evaluate each input, but also add an out-of-range value)
        sequenceNums = [0, 16, 1]
        nIns = len(sequenceNums)
        ssextra = [seq for seq in sequenceNums]
        rsextra = []
        for whichInp in range(nIns - 1, -1, -1):
            rsextra += [whichInp, OP_INPUTSEQUENCENUMBER, OP_EQUALVERIFY]
        rsextra += [nIns, OP_INPUTSEQUENCENUMBER, OP_DROP]  # Out-of-range
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=nIns, sequenceNums=sequenceNums)
        for whichInp in range(nIns):
            assert_equal(tx.vin[whichInp].nSequence, sequenceNums[whichInp])
        node.p2p.send_txs_and_test([tx0, tx], node, success=False, expect_disconnect=True,
                                   reject_reason=INPUT_INDEX_OUT_OF_RANGE_ERROR)
        mempool += [tx0.hash]
        assert_equal(node.getrawmempool(), mempool)
        self.reconnect_p2p()  # we lost the connection from above bad tx, reconnect

        # OP_INPUTSEQUENCENUMBER (3 inputs, evaluate each input, but also add an out-of-range negative value)
        sequenceNums = [37, 42, 1001]
        nIns = len(sequenceNums)
        ssextra = [seq for seq in sequenceNums]
        rsextra = []
        for whichInp in range(nIns - 1, -1, -1):
            rsextra += [whichInp, OP_INPUTSEQUENCENUMBER, OP_EQUALVERIFY]
        rsextra += [-nIns, OP_INPUTSEQUENCENUMBER, OP_DROP]  # Out-of-range, negative
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nIns=nIns, sequenceNums=sequenceNums)
        for whichInp in range(nIns):
            assert_equal(tx.vin[whichInp].nSequence, sequenceNums[whichInp])
        node.p2p.send_txs_and_test([tx0, tx], node, success=False, expect_disconnect=True,
                                   reject_reason=INPUT_INDEX_OUT_OF_RANGE_ERROR)
        mempool += [tx0.hash]
        assert_equal(node.getrawmempool(), mempool)
        self.reconnect_p2p()  # we lost the connection from above bad tx, reconnect

        # OP_OUTPUTVALUE (20 outputs, evaluate each output)
        nOuts = 20
        ssextra = []
        rsextra = []
        for whichOut in range(nOuts - 1, -1, -1):
            rsextra += [whichOut, OP_OUTPUTVALUE, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nOuts=nOuts, pushOutputValues=True)
        assert len(ssextra) == nOuts
        for whichOut in range(nOuts):
            assert_equal(tx.vout[whichOut].nValue, ssextra[whichOut])
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_OUTPUTVALUE (20 outputs, evaluate each output, but last evaluation is out of range)
        nOuts = 20
        ssextra = []
        rsextra = []
        for whichOut in range(nOuts - 1, -1, -1):
            rsextra += [whichOut, OP_OUTPUTVALUE, OP_EQUALVERIFY]
        rsextra += [nOuts, OP_OUTPUTVALUE, OP_DROP]  # Out-of-range
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nOuts=nOuts, pushOutputValues=True)
        assert len(ssextra) == nOuts
        for whichOut in range(nOuts):
            assert_equal(tx.vout[whichOut].nValue, ssextra[whichOut])
        node.p2p.send_txs_and_test([tx0, tx], node, success=False, expect_disconnect=True,
                                   reject_reason=OUTPUT_INDEX_OUT_OF_RANGE_ERROR)
        mempool += [tx0.hash]
        assert_equal(node.getrawmempool(), mempool)
        self.reconnect_p2p()  # we lost the connection from above bad tx, reconnect

        # OP_OUTPUTVALUE (20 outputs, evaluate each output, but last evaluation is out of range, negative)
        nOuts = 20
        ssextra = []
        rsextra = []
        for whichOut in range(nOuts - 1, -1, -1):
            rsextra += [whichOut, OP_OUTPUTVALUE, OP_EQUALVERIFY]
        rsextra += [-1, OP_OUTPUTVALUE, OP_DROP]  # Out-of-range (negative)
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nOuts=nOuts, pushOutputValues=True)
        assert len(ssextra) == nOuts
        for whichOut in range(nOuts):
            assert_equal(tx.vout[whichOut].nValue, ssextra[whichOut])
        node.p2p.send_txs_and_test([tx0, tx], node, success=False, expect_disconnect=True,
                                   reject_reason=OUTPUT_INDEX_OUT_OF_RANGE_ERROR)
        mempool += [tx0.hash]
        assert_equal(node.getrawmempool(), mempool)
        self.reconnect_p2p()  # we lost the connection from above bad tx, reconnect

        # OP_OUTPUTBYTECODE (20 outputs, evaluate each output)
        nOuts = 20
        ssextra = []
        rsextra = []
        for whichOut in range(nOuts - 1, -1, -1):
            rsextra += [whichOut, OP_OUTPUTBYTECODE, OP_EQUALVERIFY]
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nOuts=nOuts, pushOutputScriptPubKeys=True)
        assert len(ssextra) == nOuts
        for whichOut in range(nOuts):
            assert_equal(tx.vout[whichOut].scriptPubKey, ssextra[whichOut])
        node.p2p.send_txs_and_test([tx0, tx], node)
        mempool += [tx0.hash, tx.hash]
        spendable_txns.insert(0, tx)  # recycle the txn's output
        assert_equal(node.getrawmempool(), mempool)

        # OP_OUTPUTBYTECODE (20 outputs, evaluate each output, but last evaluation is out of range)
        nOuts = 20
        ssextra = []
        rsextra = []
        for whichOut in range(nOuts - 1, -1, -1):
            rsextra += [whichOut, OP_OUTPUTBYTECODE, OP_EQUALVERIFY]
        rsextra += [nOuts, OP_OUTPUTBYTECODE, OP_DROP]  # Out-of-range
        tx0, tx = create_fund_and_spend_tx(ssextra, rsextra, nOuts=nOuts, pushOutputScriptPubKeys=True)
        assert len(ssextra) == nOuts
        for whichOut in range(nOuts):
            assert_equal(tx.vout[whichOut].scriptPubKey, ssextra[whichOut])
        node.p2p.send_txs_and_test([tx0, tx], node, success=False, expect_disconnect=True,
                                   reject_reason=OUTPUT_INDEX_OUT_OF_RANGE_ERROR)
        mempool += [tx0.hash]
        assert_equal(node.getrawmempool(), mempool)
        self.reconnect_p2p()  # we lost the connection from above bad tx, reconnect

        # --------------------------------------------------------------------
        # Mine the mempool and ensure that all txns made it into a block
        # --------------------------------------------------------------------

        prevtiphash = node.getbestblockhash()
        tiphash = self.generatetoaddress(node, 1, node.get_deterministic_priv_key().address)[0]
        assert prevtiphash != tiphash
        assert_equal(node.getrawmempool(), [])
        blockinfo = node.getblock(tiphash, 1)
        assert all(txid in blockinfo['tx'] for txid in mempool)

        # --------------------------------------------------------------------
        # Test that scripts fail evaluation if native introspection feature is
        # disabled.
        # --------------------------------------------------------------------

        # 1. Restart the node with -reindex-chainstate (to be paranoid)
        self.restart_node(0, self.extra_args[0] + ["-reindex-chainstate=1"])
        assert_equal(node.getbestblockhash(), tiphash)

        # The below tests have been disabled since BCHN no longer has the -upgrade8activationtime argument
        # (it has activated already and the height has been hard-coded for all chains).
        """
        # 2. Restart the node with native introspection disabled
        #    We specify -reindex-chainstate=1 in order to have it re-evaluate all txns, and reject what it doesn't
        #    understand.  It should roll-back the latest block since now that block is invalid.
        self.restart_node(0, ["-acceptnonstdtxn=1", "-upgrade8activationtime=9999999999", "-expire=0",
                              "-reindex-chainstate=1"])
        assert_equal(node.getbestblockhash(), prevtiphash)

        # 3. Finally, restart the node again with introspection enabled, and reconsider the block, it should now
        #    be accepted again
        self.restart_node(0, self.extra_args[0])
        assert_equal(node.getbestblockhash(), prevtiphash)
        node.reconsiderblock(tiphash)
        assert_equal(node.getbestblockhash(), tiphash)
        """


if __name__ == '__main__':
    NativeIntrospectionTest().main()
