288 lines
8.1 KiB
Python
288 lines
8.1 KiB
Python
|
#!/usr/bin/env python3
|
||
|
# Copyright (c) 2015-2022 The Moneyrocket 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_2DIV,
|
||
|
OP_2MUL,
|
||
|
OP_AND,
|
||
|
OP_CAT,
|
||
|
OP_CHECKSIG,
|
||
|
OP_DIV,
|
||
|
OP_INVERT,
|
||
|
OP_LEFT,
|
||
|
OP_LSHIFT,
|
||
|
OP_MOD,
|
||
|
OP_MUL,
|
||
|
OP_OR,
|
||
|
OP_RETURN,
|
||
|
OP_RIGHT,
|
||
|
OP_RSHIFT,
|
||
|
OP_SUBSTR,
|
||
|
OP_XOR,
|
||
|
)
|
||
|
from test_framework.script_util import (
|
||
|
MIN_PADDING,
|
||
|
MIN_STANDARD_TX_NONWITNESS_SIZE,
|
||
|
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 moneyrocketd 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 here to make sure
|
||
|
# it is interpreted as a non-witness transaction.
|
||
|
# Otherwise the transaction will fail the
|
||
|
# "surpufluous witness" check during deserialization
|
||
|
# rather than the input count check.
|
||
|
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_RETURN] + ([OP_0] * (MIN_PADDING - 2)))))
|
||
|
assert len(tx.serialize_without_witness()) == 64
|
||
|
assert MIN_STANDARD_TX_NONWITNESS_SIZE - 1 == 64
|
||
|
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 getDisabledOpcodeTemplate(opcode):
|
||
|
""" Creates disabled opcode tx template class"""
|
||
|
def get_tx(self):
|
||
|
tx = CTransaction()
|
||
|
vin = self.valid_txin
|
||
|
vin.scriptSig = CScript([opcode])
|
||
|
tx.vin.append(vin)
|
||
|
tx.vout.append(CTxOut(1, basic_p2sh))
|
||
|
tx.calc_sha256()
|
||
|
return tx
|
||
|
|
||
|
return type('DisabledOpcode_' + str(opcode), (BadTxTemplate,), {
|
||
|
'reject_reason': "disabled opcode",
|
||
|
'expect_disconnect': True,
|
||
|
'get_tx': get_tx,
|
||
|
'valid_in_block' : True
|
||
|
})
|
||
|
|
||
|
# Disabled opcode tx templates (CVE-2010-5137)
|
||
|
DisabledOpcodeTemplates = [getDisabledOpcodeTemplate(opcode) for opcode in [
|
||
|
OP_CAT,
|
||
|
OP_SUBSTR,
|
||
|
OP_LEFT,
|
||
|
OP_RIGHT,
|
||
|
OP_INVERT,
|
||
|
OP_AND,
|
||
|
OP_OR,
|
||
|
OP_XOR,
|
||
|
OP_2MUL,
|
||
|
OP_2DIV,
|
||
|
OP_MUL,
|
||
|
OP_DIV,
|
||
|
OP_MOD,
|
||
|
OP_LSHIFT,
|
||
|
OP_RSHIFT]]
|
||
|
|
||
|
|
||
|
def iter_all_templates():
|
||
|
"""Iterate through all bad transaction template types."""
|
||
|
return BadTxTemplate.__subclasses__()
|