diff --git a/test/functional/test_framework/txtools.py b/test/functional/test_framework/txtools.py --- a/test/functional/test_framework/txtools.py +++ b/test/functional/test_framework/txtools.py @@ -1,64 +1,69 @@ #!/usr/bin/env python3 import random +import unittest -from .cdefs import MAX_TXOUT_PUBKEY_SCRIPT, MIN_TX_SIZE +from .cdefs import MIN_TX_SIZE from .messages import CTransaction, CTxOut, FromHex, ToHex -from .script import OP_RETURN, CScript +from .script import OP_RETURN, OP_TRUE, CScript + +MAX_OP_RETURN_PAYLOAD = 220 + +# MAX_OP_RETURN_RELAY as defined in standard.h: MAX_OP_RETURN_PAYLOAD +# +1 byte for OP_RETURN, +1 for OP_PUSHDATA1, +1 for VarInt (<0xfc) data length +MAX_OP_RETURN_RELAY = 3 + MAX_OP_RETURN_PAYLOAD + +VOUT_VALUE_SIZE = 8 def pad_tx(tx, pad_to_size=MIN_TX_SIZE): """ - Pad a transaction with op_return junk data until it is at least pad_to_size, or - leave it alone if it's already bigger than that. + Pad a transaction with op_return junk data until it is at least pad_to_size, + or leave it alone if it's already bigger than that. + + This function attempts to make the tx to be exactly of size pad_to_size. + There are two cases where this is not possible: + 1. The transaction is already larger than the requested size. + 2. The requested size (modulo one full OP_RETURN vout) is less than the + current size plus the minimum vout overhead of 10 bytes. """ curr_size = len(tx.serialize()) if curr_size >= pad_to_size: # Bail early txn is already big enough return - # This code attempts to pad a transaction with opreturn vouts such that - # it will be exactly pad_to_size. In order to do this we have to create - # vouts of size x (maximum OP_RETURN size - vout overhead), plus the final - # one subsumes any runoff which would be less than vout overhead. - # - # There are two cases where this is not possible: - # 1. The transaction size is between pad_to_size and pad_to_size - extrabytes - # 2. The transaction is already greater than pad_to_size - # - # Visually: - # | .. x .. | .. x .. | .. x .. | .. x + desired_size % x | - # VOUT_1 VOUT_2 VOUT_3 VOUT_4 - # txout.value + txout.pk_script bytes + op_return - extra_bytes = 8 + 1 + 1 required_padding = pad_to_size - curr_size while required_padding > 0: - # We need at least extra_bytes left over each time, or we can't - # subsume the final (and possibly undersized) iteration of the loop - padding_len = min(required_padding, - MAX_TXOUT_PUBKEY_SCRIPT - extra_bytes) - assert padding_len >= 0, "Can't pad less than 0 bytes, trying {}".format( - padding_len) - # We will end up with less than 1 UTXO of bytes after this, add - # them to this txn - next_iteration_padding = required_padding - padding_len - extra_bytes - if next_iteration_padding > 0 and next_iteration_padding < extra_bytes: - padding_len += next_iteration_padding - - # If we're at exactly, or below, extra_bytes we don't want a 1 extra - # byte padding - if padding_len <= extra_bytes: + # Smallest possible padding with an empty OP_RETURN vout: + # vout.value (8 bytes) + script length (1) + OP_RETURN (1) + if required_padding <= 10: tx.vout.append(CTxOut(0, CScript([OP_RETURN]))) - else: - # Subtract the overhead for the TxOut - padding_len -= extra_bytes - padding = random.randrange( - 1 << 8 * padding_len - 2, 1 << 8 * padding_len - 1) - tx.vout.append( - CTxOut(0, CScript([OP_RETURN, padding]))) - - curr_size = len(tx.serialize()) - required_padding = pad_to_size - curr_size - assert curr_size >= pad_to_size, "{} !>= {}".format(curr_size, pad_to_size) + break + + # The total padding size, for a payload < 0x4c, is: + # vout.value (8 bytes) + script_length (1) + + # OP_RETURN (1) + data length (1) + data + data_size = min(required_padding - VOUT_VALUE_SIZE - 3, + MAX_OP_RETURN_PAYLOAD) + + script_operations = [] + if data_size == 0x4c: + # Special case for data size exactly at the PUSHDATA transition: + # we cannot reach the required size with only OP_RETURN, so prepend + # another 1 byte operation and remove 1 byte from the payload. + data_size -= 1 + script_operations += [OP_TRUE] + elif data_size > 0x4c: + # Transition from simple PUSHDATA to requiring an additional + # OP_PUSHDATA1 op code: remove 1 byte from the payload. + data_size -= 1 + + script_operations += [ + OP_RETURN, + bytes([random.randrange(0xff) for _ in range(data_size)]) + ] + tx.vout.append(CTxOut(0, CScript(script_operations))) + required_padding -= MAX_OP_RETURN_RELAY + VOUT_VALUE_SIZE + tx.rehash() @@ -70,3 +75,72 @@ FromHex(tx, rawtx_hex) pad_tx(tx, min_size) return ToHex(tx) + + +class TestFrameworkScript(unittest.TestCase): + def test_pad_raw_tx(self): + raw_tx = ( + "0100000001dd22777f85ab958c065cabced6115c4a2604abb9a2273f0eedce14a" + "55c7b1201000000000201510000000001ebf802950000000017a914da1745e9b5" + "49bd0bfa1a569971c77eba30cd5a4b8700000000" + ) + + # Helper functions + def rawtx_length(rawtx): + return len(bytes.fromhex(rawtx)) + + def test_size(requested_size, expected_size): + self.assertEqual( + rawtx_length(pad_raw_tx(raw_tx, requested_size)), + expected_size) + + def test_size_equal(size): + test_size(requested_size=size, + expected_size=size) + + def test_new_vout(start_size): + """When a new vout is added, check that the first 10 transactions + have exactly 10 bytes more than the last good size. + """ + for i in range(10): + test_size(requested_size=start_size + i, + expected_size=start_size + 9) + + self.assertEqual(rawtx_length(raw_tx), 85) + + # The size is never reduced. + for size in [-1, 0, 1, 83, 84, 85]: + test_size(size, expected_size=85) + + # First new vout: the next 10 are overpadded to 95 bytes. + test_new_vout(86) + + # Larger sizes are exact, until the first OP_RETURN is full + for size in range(96, 85 + MAX_OP_RETURN_RELAY + VOUT_VALUE_SIZE + 1): + test_size_equal(size) + + # Second new vout + test_new_vout(85 + MAX_OP_RETURN_RELAY + VOUT_VALUE_SIZE + 1) + + # Second range of correctly padded transactions. + for size in range(85 + MAX_OP_RETURN_RELAY + VOUT_VALUE_SIZE + 11, + 85 + 2 * (MAX_OP_RETURN_RELAY + VOUT_VALUE_SIZE) + 1): + test_size_equal(size) + + # Third short range of overpadded transactions + test_new_vout(85 + 2 * (MAX_OP_RETURN_RELAY + VOUT_VALUE_SIZE) + 1) + + # Third long range of correctly padded tx + test_size_equal(85 + 2 * (MAX_OP_RETURN_RELAY + VOUT_VALUE_SIZE) + 11) + test_size_equal(85 + 3 * (MAX_OP_RETURN_RELAY + VOUT_VALUE_SIZE)) + + # Fourth short range of overpadded transactions + test_new_vout(85 + 3 * (MAX_OP_RETURN_RELAY + VOUT_VALUE_SIZE) + 1) + + # etc + test_size_equal(85 + 3 * (MAX_OP_RETURN_RELAY + VOUT_VALUE_SIZE) + 11) + + # Test a range of very large sizes covering 2 new vout + for size in range(7250, 7750): + self.assertGreaterEqual(rawtx_length(pad_raw_tx(raw_tx, size)), + size) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -80,6 +80,7 @@ "messages", "muhash", "script", + "txtools", "util", ]