Skip to content

Commit 8c5b364

Browse files
committed
Relax MIN_STANDARD_TX_NONWITNESS_SIZE to 65 non-witness bytes
Since the original fix was set to be a "reasonable" transaction to reduce allocations and the true motivation later revealed, it makes sense to relax this check to something more principled. There are more exotic transaction patterns that could take advantage of a relaxed requirement, such as 1 input, 1 output OP_RETURN to burn a utxo to fees for CPFP purposes when change isn't practical. Two changes could be accomplished: 1) Anything not 64 bytes could be allowed 2) Anything above 64 bytes could be allowed In the Great Consensus Cleanup, suggestion (2) was the route taken. It would not allow an "empty" OP_RETURN but would reduce the required padding from 22 bytes to 5. The functional test is also modified to test the actual case we care about: 64 bytes
1 parent 9ca39d6 commit 8c5b364

File tree

5 files changed

+57
-19
lines changed

5 files changed

+57
-19
lines changed

src/policy/policy.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ static constexpr unsigned int DEFAULT_BLOCK_MAX_WEIGHT{MAX_BLOCK_WEIGHT - 4000};
2525
static constexpr unsigned int DEFAULT_BLOCK_MIN_TX_FEE{1000};
2626
/** The maximum weight for transactions we're willing to relay/mine */
2727
static constexpr unsigned int MAX_STANDARD_TX_WEIGHT{400000};
28-
/** The minimum non-witness size for transactions we're willing to relay/mine (1 segwit input + 1 P2WPKH output = 82 bytes) */
29-
static constexpr unsigned int MIN_STANDARD_TX_NONWITNESS_SIZE{82};
28+
/** The minimum non-witness size for transactions we're willing to relay/mine: one larger than 64 */
29+
static constexpr unsigned int MIN_STANDARD_TX_NONWITNESS_SIZE{65};
3030
/** Maximum number of signature check operations in an IsStandard() P2SH script */
3131
static constexpr unsigned int MAX_P2SH_SIGOPS{15};
3232
/** The maximum number of sigops we're willing to relay/mine in a single tx */

src/validation.cpp

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -696,10 +696,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
696696
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, reason);
697697
}
698698

699-
// Do not work on transactions that are too small.
700-
// A transaction with 1 segwit input and 1 P2WPHK output has non-witness size of 82 bytes.
701-
// Transactions smaller than this are not relayed to mitigate CVE-2017-12842 by not relaying
702-
// 64-byte transactions.
699+
// Transactions smaller than 65 non-witness bytes are not relayed to mitigate CVE-2017-12842.
703700
if (::GetSerializeSize(tx, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS) < MIN_STANDARD_TX_NONWITNESS_SIZE)
704701
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "tx-size-small");
705702

test/functional/data/invalid_txs.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,19 @@
4646
OP_MOD,
4747
OP_MUL,
4848
OP_OR,
49+
OP_RETURN,
4950
OP_RIGHT,
5051
OP_RSHIFT,
5152
OP_SUBSTR,
52-
OP_TRUE,
5353
OP_XOR,
5454
)
5555
from test_framework.script_util import (
56+
MIN_PADDING,
57+
MIN_STANDARD_TX_NONWITNESS_SIZE,
5658
script_to_p2sh_script,
5759
)
5860
basic_p2sh = script_to_p2sh_script(CScript([OP_0]))
5961

60-
6162
class BadTxTemplate:
6263
"""Allows simple construction of a certain kind of invalid tx. Base class to be subclassed."""
6364
__metaclass__ = abc.ABCMeta
@@ -122,7 +123,9 @@ class SizeTooSmall(BadTxTemplate):
122123
def get_tx(self):
123124
tx = CTransaction()
124125
tx.vin.append(self.valid_txin)
125-
tx.vout.append(CTxOut(0, CScript([OP_TRUE])))
126+
tx.vout.append(CTxOut(0, CScript([OP_RETURN] + ([OP_0] * (MIN_PADDING - 2)))))
127+
assert len(tx.serialize_without_witness()) == 64
128+
assert MIN_STANDARD_TX_NONWITNESS_SIZE - 1 == 64
126129
tx.calc_sha256()
127130
return tx
128131

test/functional/mempool_accept.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
MAX_BIP125_RBF_SEQUENCE,
1515
COIN,
1616
COutPoint,
17+
CTransaction,
1718
CTxIn,
19+
CTxInWitness,
1820
CTxOut,
1921
MAX_BLOCK_WEIGHT,
2022
MAX_MONEY,
@@ -26,13 +28,19 @@
2628
OP_0,
2729
OP_HASH160,
2830
OP_RETURN,
31+
OP_TRUE,
2932
)
3033
from test_framework.script_util import (
34+
DUMMY_MIN_OP_RETURN_SCRIPT,
3135
keys_to_multisig_script,
36+
MIN_PADDING,
37+
MIN_STANDARD_TX_NONWITNESS_SIZE,
3238
script_to_p2sh_script,
39+
script_to_p2wsh_script,
3340
)
3441
from test_framework.util import (
3542
assert_equal,
43+
assert_greater_than,
3644
assert_raises_rpc_error,
3745
)
3846
from test_framework.wallet import MiniWallet
@@ -333,6 +341,35 @@ def run_test(self):
333341
maxfeerate=0,
334342
)
335343

344+
# Prep for tiny-tx tests with wsh(OP_TRUE) output
345+
seed_tx = self.wallet.send_to(from_node=node, scriptPubKey=script_to_p2wsh_script(CScript([OP_TRUE])), amount=COIN)
346+
self.generate(node, 1)
347+
348+
self.log.info('A tiny transaction(in non-witness bytes) that is disallowed')
349+
tx = CTransaction()
350+
tx.vin.append(CTxIn(COutPoint(int(seed_tx[0], 16), seed_tx[1]), b"", SEQUENCE_FINAL))
351+
tx.wit.vtxinwit = [CTxInWitness()]
352+
tx.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE])]
353+
tx.vout.append(CTxOut(0, CScript([OP_RETURN] + ([OP_0] * (MIN_PADDING - 2)))))
354+
# Note it's only non-witness size that matters!
355+
assert_equal(len(tx.serialize_without_witness()), 64)
356+
assert_equal(MIN_STANDARD_TX_NONWITNESS_SIZE - 1, 64)
357+
assert_greater_than(len(tx.serialize()), 64)
358+
359+
self.check_mempool_result(
360+
result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'tx-size-small'}],
361+
rawtxs=[tx.serialize().hex()],
362+
maxfeerate=0,
363+
)
364+
365+
self.log.info('Minimally-small transaction(in non-witness bytes) that is allowed')
366+
tx.vout[0] = CTxOut(COIN - 1000, DUMMY_MIN_OP_RETURN_SCRIPT)
367+
assert_equal(len(tx.serialize_without_witness()), MIN_STANDARD_TX_NONWITNESS_SIZE)
368+
self.check_mempool_result(
369+
result_expected=[{'txid': tx.rehash(), 'allowed': True, 'vsize': tx.get_vsize(), 'fees': { 'base': Decimal('0.00001000')}}],
370+
rawtxs=[tx.serialize().hex()],
371+
maxfeerate=0,
372+
)
336373

337374
if __name__ == '__main__':
338375
MempoolAcceptanceTest().main()

test/functional/test_framework/script_util.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
OP_EQUAL,
1414
OP_EQUALVERIFY,
1515
OP_HASH160,
16+
OP_RETURN,
1617
hash160,
1718
sha256,
1819
)
1920

2021
# To prevent a "tx-size-small" policy rule error, a transaction has to have a
21-
# non-witness size of at least 82 bytes (MIN_STANDARD_TX_NONWITNESS_SIZE in
22+
# non-witness size of at least 65 bytes (MIN_STANDARD_TX_NONWITNESS_SIZE in
2223
# src/policy/policy.h). Considering a Tx with the smallest possible single
2324
# input (blank, empty scriptSig), and with an output omitting the scriptPubKey,
2425
# we get to a minimum size of 60 bytes:
@@ -28,15 +29,15 @@
2829
# Output: 8 [Amount] + 1 [scriptPubKeyLen] = 9 bytes
2930
#
3031
# Hence, the scriptPubKey of the single output has to have a size of at
31-
# least 22 bytes, which corresponds to the size of a P2WPKH scriptPubKey.
32-
# The following script constant consists of a single push of 21 bytes of 'a':
33-
# <PUSH_21> <21-bytes of 'a'>
34-
# resulting in a 22-byte size. It should be used whenever (small) fake
35-
# scriptPubKeys are needed, to guarantee that the minimum transaction size is
36-
# met.
37-
DUMMY_P2WPKH_SCRIPT = CScript([b'a' * 21])
38-
DUMMY_2_P2WPKH_SCRIPT = CScript([b'b' * 21])
39-
32+
# least 5 bytes.
33+
MIN_STANDARD_TX_NONWITNESS_SIZE = 65
34+
MIN_PADDING = MIN_STANDARD_TX_NONWITNESS_SIZE - 10 - 41 - 9
35+
assert MIN_PADDING == 5
36+
37+
# This script cannot be spent, allowing dust output values under
38+
# standardness checks
39+
DUMMY_MIN_OP_RETURN_SCRIPT = CScript([OP_RETURN] + ([OP_0] * (MIN_PADDING - 1)))
40+
assert len(DUMMY_MIN_OP_RETURN_SCRIPT) == MIN_PADDING
4041

4142
def key_to_p2pk_script(key):
4243
key = check_key(key)

0 commit comments

Comments
 (0)