#!/usr/bin/env python3
# Copyright (c) 2015-2020 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""
Templates for constructing various sorts of invalid transactions.

These templates (or an iterator over all of them) can be reused in different
contexts to test using a number of invalid transaction types.

Hopefully this makes it easier to get coverage of a full variety of tx
validation checks through different interfaces (AcceptBlock, AcceptToMemPool,
etc.) without repeating ourselves.

Invalid tx cases not covered here can be found by running:

    $ diff \
      <(grep -IREho "bad-txns[a-zA-Z-]+" src | sort -u) \
      <(grep -IEho "bad-txns[a-zA-Z-]+" test/functional/data/invalid_txs.py | sort -u)

"""
import abc

from typing import Optional
from test_framework.messages import (
    COutPoint,
    CTransaction,
    CTxIn,
    CTxOut,
    MAX_MONEY,
    SEQUENCE_FINAL,
)
from test_framework.blocktools import create_tx_with_script, MAX_BLOCK_SIGOPS
from test_framework.script import (
    CScript,
    OP_0,
    OP_CHECKSIG,
    OP_TRUE,
)
from test_framework.script_util import (
    script_to_p2sh_script,
)
basic_p2sh = script_to_p2sh_script(CScript([OP_0]))


class BadTxTemplate:
    """Allows simple construction of a certain kind of invalid tx. Base class to be subclassed."""
    __metaclass__ = abc.ABCMeta

    # The expected error code given by bitcoind upon submission of the tx.
    reject_reason: Optional[str] = ""

    # Only specified if it differs from mempool acceptance error.
    block_reject_reason = ""

    # Do we expect to be disconnected after submitting this tx?
    expect_disconnect = False

    # Is this tx considered valid when included in a block, but not for acceptance into
    # the mempool (i.e. does it violate policy but not consensus)?
    valid_in_block = False

    def __init__(self, *, spend_tx=None, spend_block=None):
        self.spend_tx = spend_block.vtx[0] if spend_block else spend_tx
        self.spend_avail = sum(o.nValue for o in self.spend_tx.vout)
        self.valid_txin = CTxIn(COutPoint(self.spend_tx.sha256, 0), b"", SEQUENCE_FINAL)

    @abc.abstractmethod
    def get_tx(self, *args, **kwargs):
        """Return a CTransaction that is invalid per the subclass."""
        pass


class OutputMissing(BadTxTemplate):
    reject_reason = "bad-txns-vout-empty"
    expect_disconnect = True

    def get_tx(self):
        tx = CTransaction()
        tx.vin.append(self.valid_txin)
        tx.calc_sha256()
        return tx


class InputMissing(BadTxTemplate):
    reject_reason = "bad-txns-vin-empty"
    expect_disconnect = True

    # We use a blank transaction to align with bitcoin's implementation
    def get_tx(self):
        tx = CTransaction()
        tx.calc_sha256()
        return tx


# The following check prevents exploit of lack of merkle
# tree depth commitment (CVE-2017-12842)
class SizeTooSmall(BadTxTemplate):
    reject_reason = "tx-size-small"
    expect_disconnect = False
    valid_in_block = True

    def get_tx(self):
        tx = CTransaction()
        tx.vin.append(self.valid_txin)
        tx.vout.append(CTxOut(0, CScript([OP_TRUE])))
        tx.calc_sha256()
        return tx


class BadInputOutpointIndex(BadTxTemplate):
    # Won't be rejected - nonexistent outpoint index is treated as an orphan since the coins
    # database can't distinguish between spent outpoints and outpoints which never existed.
    reject_reason = None
    expect_disconnect = False

    def get_tx(self):
        num_indices = len(self.spend_tx.vin)
        bad_idx = num_indices + 100

        tx = CTransaction()
        tx.vin.append(CTxIn(COutPoint(self.spend_tx.sha256, bad_idx), b"", SEQUENCE_FINAL))
        tx.vout.append(CTxOut(0, basic_p2sh))
        tx.calc_sha256()
        return tx


class DuplicateInput(BadTxTemplate):
    reject_reason = 'bad-txns-inputs-duplicate'
    expect_disconnect = True

    def get_tx(self):
        tx = CTransaction()
        tx.vin.append(self.valid_txin)
        tx.vin.append(self.valid_txin)
        tx.vout.append(CTxOut(1, basic_p2sh))
        tx.calc_sha256()
        return tx


class PrevoutNullInput(BadTxTemplate):
    reject_reason = 'bad-txns-prevout-null'
    expect_disconnect = True

    def get_tx(self):
        tx = CTransaction()
        tx.vin.append(self.valid_txin)
        tx.vin.append(CTxIn(COutPoint(hash=0, n=0xffffffff)))
        tx.vout.append(CTxOut(1, basic_p2sh))
        tx.calc_sha256()
        return tx


class NonexistentInput(BadTxTemplate):
    reject_reason = None  # Added as an orphan tx.
    expect_disconnect = False

    def get_tx(self):
        tx = CTransaction()
        tx.vin.append(CTxIn(COutPoint(self.spend_tx.sha256 + 1, 0), b"", SEQUENCE_FINAL))
        tx.vin.append(self.valid_txin)
        tx.vout.append(CTxOut(1, basic_p2sh))
        tx.calc_sha256()
        return tx


class SpendTooMuch(BadTxTemplate):
    reject_reason = 'bad-txns-in-belowout'
    expect_disconnect = True

    def get_tx(self):
        return create_tx_with_script(
            self.spend_tx, 0, script_pub_key=basic_p2sh, amount=(self.spend_avail + 1))


class CreateNegative(BadTxTemplate):
    reject_reason = 'bad-txns-vout-negative'
    expect_disconnect = True

    def get_tx(self):
        return create_tx_with_script(self.spend_tx, 0, amount=-1)


class CreateTooLarge(BadTxTemplate):
    reject_reason = 'bad-txns-vout-toolarge'
    expect_disconnect = True

    def get_tx(self):
        return create_tx_with_script(self.spend_tx, 0, amount=MAX_MONEY + 1)


class CreateSumTooLarge(BadTxTemplate):
    reject_reason = 'bad-txns-txouttotal-toolarge'
    expect_disconnect = True

    def get_tx(self):
        tx = create_tx_with_script(self.spend_tx, 0, amount=MAX_MONEY)
        tx.vout = [tx.vout[0]] * 2
        tx.calc_sha256()
        return tx


class InvalidOPIFConstruction(BadTxTemplate):
    reject_reason = "mandatory-script-verify-flag-failed (Invalid OP_IF construction)"
    expect_disconnect = True
    valid_in_block = True

    def get_tx(self):
        return create_tx_with_script(
            self.spend_tx, 0, script_sig=b'\x64' * 35,
            amount=(self.spend_avail // 2))


class TooManySigops(BadTxTemplate):
    reject_reason = "bad-txns-too-many-sigops"
    block_reject_reason = "bad-blk-sigops, out-of-bounds SigOpCount"
    expect_disconnect = False

    def get_tx(self):
        lotsa_checksigs = CScript([OP_CHECKSIG] * (MAX_BLOCK_SIGOPS))
        return create_tx_with_script(
            self.spend_tx, 0,
            script_pub_key=lotsa_checksigs,
            amount=1)


def iter_all_templates():
    """Iterate through all bad transaction template types."""
    return BadTxTemplate.__subclasses__()
