Skip to content

Commit 59e3877

Browse files
committed
test: add invalid tx templates for use in functional tests
Add templates for easily constructing different kinds of invalid transactions and use them in feature_block and p2p_invalid_tx.
1 parent a4eaaa6 commit 59e3877

File tree

5 files changed

+254
-20
lines changed

5 files changed

+254
-20
lines changed

test/functional/data/invalid_txs.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2015-2018 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""
6+
Templates for constructing various sorts of invalid transactions.
7+
8+
These templates (or an iterator over all of them) can be reused in different
9+
contexts to test using a number of invalid transaction types.
10+
11+
Hopefully this makes it easier to get coverage of a full variety of tx
12+
validation checks through different interfaces (AcceptBlock, AcceptToMemPool,
13+
etc.) without repeating ourselves.
14+
15+
Invalid tx cases not covered here can be found by running:
16+
17+
$ diff \
18+
<(grep -IREho "bad-txns[a-zA-Z-]+" src | sort -u) \
19+
<(grep -IEho "bad-txns[a-zA-Z-]+" test/functional/data/invalid_txs.py | sort -u)
20+
21+
"""
22+
import abc
23+
24+
from test_framework.messages import CTransaction, CTxIn, CTxOut, COutPoint
25+
from test_framework import script as sc
26+
from test_framework.blocktools import create_tx_with_script, MAX_BLOCK_SIGOPS
27+
28+
basic_p2sh = sc.CScript([sc.OP_HASH160, sc.hash160(sc.CScript([sc.OP_0])), sc.OP_EQUAL])
29+
30+
31+
class BadTxTemplate:
32+
"""Allows simple construction of a certain kind of invalid tx. Base class to be subclassed."""
33+
__metaclass__ = abc.ABCMeta
34+
35+
# The expected error code given by bitcoind upon submission of the tx.
36+
reject_reason = ""
37+
38+
# Only specified if it differs from mempool acceptance error.
39+
block_reject_reason = ""
40+
41+
# Do we expect to be disconnected after submitting this tx?
42+
expect_disconnect = False
43+
44+
# Is this tx considered valid when included in a block, but not for acceptance into
45+
# the mempool (i.e. does it violate policy but not consensus)?
46+
valid_in_block = False
47+
48+
def __init__(self, *, spend_tx=None, spend_block=None):
49+
self.spend_tx = spend_block.vtx[0] if spend_block else spend_tx
50+
self.spend_avail = sum(o.nValue for o in self.spend_tx.vout)
51+
self.valid_txin = CTxIn(COutPoint(self.spend_tx.sha256, 0), b"", 0xffffffff)
52+
53+
@abc.abstractmethod
54+
def get_tx(self, *args, **kwargs):
55+
"""Return a CTransaction that is invalid per the subclass."""
56+
pass
57+
58+
59+
class OutputMissing(BadTxTemplate):
60+
reject_reason = "bad-txns-vout-empty"
61+
expect_disconnect = False
62+
63+
def get_tx(self):
64+
tx = CTransaction()
65+
tx.vin.append(self.valid_txin)
66+
tx.calc_sha256()
67+
return tx
68+
69+
70+
class InputMissing(BadTxTemplate):
71+
reject_reason = "bad-txns-vin-empty"
72+
expect_disconnect = False
73+
74+
def get_tx(self):
75+
tx = CTransaction()
76+
tx.vout.append(CTxOut(0, sc.CScript([sc.OP_TRUE] * 100)))
77+
tx.calc_sha256()
78+
return tx
79+
80+
81+
class SizeTooSmall(BadTxTemplate):
82+
reject_reason = "tx-size-small"
83+
expect_disconnect = False
84+
valid_in_block = True
85+
86+
def get_tx(self):
87+
tx = CTransaction()
88+
tx.vin.append(self.valid_txin)
89+
tx.vout.append(CTxOut(0, sc.CScript([sc.OP_TRUE])))
90+
tx.calc_sha256()
91+
return tx
92+
93+
94+
class BadInputOutpointIndex(BadTxTemplate):
95+
# Won't be rejected - nonexistent outpoint index is treated as an orphan since the coins
96+
# database can't distinguish between spent outpoints and outpoints which never existed.
97+
reject_reason = None
98+
expect_disconnect = False
99+
100+
def get_tx(self):
101+
num_indices = len(self.spend_tx.vin)
102+
bad_idx = num_indices + 100
103+
104+
tx = CTransaction()
105+
tx.vin.append(CTxIn(COutPoint(self.spend_tx.sha256, bad_idx), b"", 0xffffffff))
106+
tx.vout.append(CTxOut(0, basic_p2sh))
107+
tx.calc_sha256()
108+
return tx
109+
110+
111+
class DuplicateInput(BadTxTemplate):
112+
reject_reason = 'bad-txns-inputs-duplicate'
113+
expect_disconnect = True
114+
115+
def get_tx(self):
116+
tx = CTransaction()
117+
tx.vin.append(self.valid_txin)
118+
tx.vin.append(self.valid_txin)
119+
tx.vout.append(CTxOut(1, basic_p2sh))
120+
tx.calc_sha256()
121+
return tx
122+
123+
124+
class NonexistentInput(BadTxTemplate):
125+
reject_reason = None # Added as an orphan tx.
126+
expect_disconnect = False
127+
128+
def get_tx(self):
129+
tx = CTransaction()
130+
tx.vin.append(CTxIn(COutPoint(self.spend_tx.sha256 + 1, 0), b"", 0xffffffff))
131+
tx.vin.append(self.valid_txin)
132+
tx.vout.append(CTxOut(1, basic_p2sh))
133+
tx.calc_sha256()
134+
return tx
135+
136+
137+
class SpendTooMuch(BadTxTemplate):
138+
reject_reason = 'bad-txns-in-belowout'
139+
expect_disconnect = True
140+
141+
def get_tx(self):
142+
return create_tx_with_script(
143+
self.spend_tx, 0, script_pub_key=basic_p2sh, amount=(self.spend_avail + 1))
144+
145+
146+
class SpendNegative(BadTxTemplate):
147+
reject_reason = 'bad-txns-vout-negative'
148+
expect_disconnect = True
149+
150+
def get_tx(self):
151+
return create_tx_with_script(self.spend_tx, 0, amount=-1)
152+
153+
154+
class InvalidOPIFConstruction(BadTxTemplate):
155+
reject_reason = "mandatory-script-verify-flag-failed (Invalid OP_IF construction)"
156+
expect_disconnect = True
157+
valid_in_block = True
158+
159+
def get_tx(self):
160+
return create_tx_with_script(
161+
self.spend_tx, 0, script_sig=b'\x64' * 35,
162+
amount=(self.spend_avail // 2))
163+
164+
165+
class TooManySigops(BadTxTemplate):
166+
reject_reason = "bad-txns-too-many-sigops"
167+
block_reject_reason = "bad-blk-sigops, out-of-bounds SigOpCount"
168+
expect_disconnect = False
169+
170+
def get_tx(self):
171+
lotsa_checksigs = sc.CScript([sc.OP_CHECKSIG] * (MAX_BLOCK_SIGOPS))
172+
return create_tx_with_script(
173+
self.spend_tx, 0,
174+
script_pub_key=lotsa_checksigs,
175+
amount=1)
176+
177+
178+
def iter_all_templates():
179+
"""Iterate through all bad transaction template types."""
180+
return BadTxTemplate.__subclasses__()

test/functional/feature_block.py

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
import struct
88
import time
99

10-
from test_framework.blocktools import create_block, create_coinbase, create_tx_with_script, get_legacy_sigopcount_block
10+
from test_framework.blocktools import (
11+
create_block,
12+
create_coinbase,
13+
create_tx_with_script,
14+
get_legacy_sigopcount_block,
15+
MAX_BLOCK_SIGOPS,
16+
)
1117
from test_framework.key import CECKey
1218
from test_framework.messages import (
1319
CBlock,
@@ -45,8 +51,7 @@
4551
)
4652
from test_framework.test_framework import BitcoinTestFramework
4753
from test_framework.util import assert_equal
48-
49-
MAX_BLOCK_SIGOPS = 20000
54+
from data import invalid_txs
5055

5156
# Use this class for tests that require behavior other than normal "mininode" behavior.
5257
# For now, it is used to serialize a bloated varint (b64).
@@ -95,16 +100,21 @@ def run_test(self):
95100
self.save_spendable_output()
96101
self.sync_blocks([b0])
97102

103+
# These constants chosen specifically to trigger an immature coinbase spend
104+
# at a certain time below.
105+
NUM_BUFFER_BLOCKS_TO_GENERATE = 99
106+
NUM_OUTPUTS_TO_COLLECT = 33
107+
98108
# Allow the block to mature
99109
blocks = []
100-
for i in range(99):
101-
blocks.append(self.next_block(5000 + i))
110+
for i in range(NUM_BUFFER_BLOCKS_TO_GENERATE):
111+
blocks.append(self.next_block("maturitybuffer.{}".format(i)))
102112
self.save_spendable_output()
103113
self.sync_blocks(blocks)
104114

105115
# collect spendable outputs now to avoid cluttering the code later on
106116
out = []
107-
for i in range(33):
117+
for i in range(NUM_OUTPUTS_TO_COLLECT):
108118
out.append(self.get_spendable_output())
109119

110120
# Start by building a couple of blocks on top (which output is spent is
@@ -116,7 +126,39 @@ def run_test(self):
116126
b2 = self.next_block(2, spend=out[1])
117127
self.save_spendable_output()
118128

119-
self.sync_blocks([b1, b2])
129+
self.sync_blocks([b1, b2], timeout=4)
130+
131+
# Select a txn with an output eligible for spending. This won't actually be spent,
132+
# since we're testing submission of a series of blocks with invalid txns.
133+
attempt_spend_tx = out[2]
134+
135+
# Submit blocks for rejection, each of which contains a single transaction
136+
# (aside from coinbase) which should be considered invalid.
137+
for TxTemplate in invalid_txs.iter_all_templates():
138+
template = TxTemplate(spend_tx=attempt_spend_tx)
139+
140+
# Something about the serialization code for missing inputs creates
141+
# a different hash in the test client than on bitcoind, resulting
142+
# in a mismatching merkle root during block validation.
143+
# Skip until we figure out what's going on.
144+
if TxTemplate == invalid_txs.InputMissing:
145+
continue
146+
if template.valid_in_block:
147+
continue
148+
149+
self.log.info("Reject block with invalid tx: %s", TxTemplate.__name__)
150+
blockname = "for_invalid.%s" % TxTemplate.__name__
151+
badblock = self.next_block(blockname)
152+
badtx = template.get_tx()
153+
self.sign_tx(badtx, attempt_spend_tx)
154+
badtx.rehash()
155+
badblock = self.update_block(blockname, [badtx])
156+
self.sync_blocks(
157+
[badblock], success=False,
158+
reject_reason=(template.block_reject_reason or template.reject_reason),
159+
reconnect=True, timeout=2)
160+
161+
self.move_tip(2)
120162

121163
# Fork like this:
122164
#
@@ -1288,7 +1330,7 @@ def update_block(self, block_number, new_transactions):
12881330
self.blocks[block_number] = block
12891331
return block
12901332

1291-
def bootstrap_p2p(self):
1333+
def bootstrap_p2p(self, timeout=10):
12921334
"""Add a P2P connection to the node.
12931335
12941336
Helper to connect and wait for version handshake."""
@@ -1299,15 +1341,15 @@ def bootstrap_p2p(self):
12991341
# an INV for the next block and receive two getheaders - one for the
13001342
# IBD and one for the INV. We'd respond to both and could get
13011343
# unexpectedly disconnected if the DoS score for that error is 50.
1302-
self.nodes[0].p2p.wait_for_getheaders(timeout=5)
1344+
self.nodes[0].p2p.wait_for_getheaders(timeout=timeout)
13031345

1304-
def reconnect_p2p(self):
1346+
def reconnect_p2p(self, timeout=60):
13051347
"""Tear down and bootstrap the P2P connection to the node.
13061348
13071349
The node gets disconnected several times in this test. This helper
13081350
method reconnects the p2p and restarts the network thread."""
13091351
self.nodes[0].disconnect_p2ps()
1310-
self.bootstrap_p2p()
1352+
self.bootstrap_p2p(timeout=timeout)
13111353

13121354
def sync_blocks(self, blocks, success=True, reject_reason=None, force_send=False, reconnect=False, timeout=60):
13131355
"""Sends blocks to test node. Syncs and verifies that tip has advanced to most recent block.
@@ -1316,7 +1358,7 @@ def sync_blocks(self, blocks, success=True, reject_reason=None, force_send=False
13161358
self.nodes[0].p2p.send_blocks_and_test(blocks, self.nodes[0], success=success, reject_reason=reject_reason, force_send=force_send, timeout=timeout, expect_disconnect=reconnect)
13171359

13181360
if reconnect:
1319-
self.reconnect_p2p()
1361+
self.reconnect_p2p(timeout=timeout)
13201362

13211363

13221364
if __name__ == '__main__':

test/functional/p2p_invalid_tx.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""Test node responses to invalid transactions.
66
77
In this test we connect to one node over p2p, and test tx requests."""
8-
from test_framework.blocktools import create_block, create_coinbase, create_tx_with_script
8+
from test_framework.blocktools import create_block, create_coinbase
99
from test_framework.messages import (
1010
COIN,
1111
COutPoint,
@@ -19,6 +19,7 @@
1919
assert_equal,
2020
wait_until,
2121
)
22+
from data import invalid_txs
2223

2324

2425
class InvalidTxRequestTest(BitcoinTestFramework):
@@ -63,12 +64,21 @@ def run_test(self):
6364
self.log.info("Mature the block.")
6465
self.nodes[0].generatetoaddress(100, self.nodes[0].get_deterministic_priv_key().address)
6566

66-
# b'\x64' is OP_NOTIF
67-
# Transaction will be rejected with code 16 (REJECT_INVALID)
68-
# and we get disconnected immediately
69-
self.log.info('Test a transaction that is rejected')
70-
tx1 = create_tx_with_script(block1.vtx[0], 0, script_sig=b'\x64' * 35, amount=50 * COIN - 12000)
71-
node.p2p.send_txs_and_test([tx1], node, success=False, expect_disconnect=True)
67+
# Iterate through a list of known invalid transaction types, ensuring each is
68+
# rejected. Some are consensus invalid and some just violate policy.
69+
for BadTxTemplate in invalid_txs.iter_all_templates():
70+
self.log.info("Testing invalid transaction: %s", BadTxTemplate.__name__)
71+
template = BadTxTemplate(spend_block=block1)
72+
tx = template.get_tx()
73+
node.p2p.send_txs_and_test(
74+
[tx], node, success=False,
75+
expect_disconnect=template.expect_disconnect,
76+
reject_reason=template.reject_reason,
77+
)
78+
79+
if template.expect_disconnect:
80+
self.log.info("Reconnecting to peer")
81+
self.reconnect_p2p()
7282

7383
# Make two p2p connections to provide the node with orphans
7484
# * p2ps[0] will send valid orphan txs (one with low fee)

test/functional/test_framework/blocktools.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
from .util import assert_equal
4242
from io import BytesIO
4343

44+
MAX_BLOCK_SIGOPS = 20000
45+
4446
# From BIP141
4547
WITNESS_COMMITMENT_HEADER = b"\xaa\x21\xa9\xed"
4648

test/lint/lint-python-dead-code.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ fi
1616
vulture \
1717
--min-confidence 60 \
1818
--ignore-names "argtypes,connection_lost,connection_made,converter,data_received,daemon,errcheck,get_ecdh_key,get_privkey,is_compressed,is_fullyvalid,msg_generic,on_*,optionxform,restype,set_privkey" \
19-
$(git ls-files -- "*.py" ":(exclude)contrib/")
19+
$(git ls-files -- "*.py" ":(exclude)contrib/" ":(exclude)test/functional/data/invalid_txs.py")

0 commit comments

Comments
 (0)