Skip to content

Commit dadf15f

Browse files
committed
Merge bitcoin/bitcoin#33050: net, validation: don't punish peers for consensus-invalid txs
876dbdf tests: drop expect_disconnect behaviour for tx relay (Anthony Towns) b29ae9e validation: only check input scripts once (Anthony Towns) 266dd0e net_processing: drop MaybePunishNodeForTx (Anthony Towns) Pull request description: Because we do not discourage nodes for transactions we consider non-standard, we don't get any DoS protection from this check in adversarial scenarios, so remove the check entirely both to simplify the code and reduce the risk of splitting the network due to changes in tx relay policy. Then, because we no longer make use of the distinction between consensus and standardness failures during script validation, don't re-validate each script with only-consensus rules, reducing the cost to us of transactions that we won't relay. ACKs for top commit: achow101: ACK 876dbdf darosior: re-ACK 876dbdf sipa: re-ACK 876dbdf glozow: ACK 876dbdf Tree-SHA512: 8bb0395766dde54fc48f7077b80b88e35581aa6e3054d6d65735965147abefffa7348f0850bb3d46f6c2541fd384ecd40a00a57fa653adabff8a35582e2d1811
2 parents 73972d5 + 876dbdf commit dadf15f

14 files changed

+63
-140
lines changed

src/net_processing.cpp

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -583,12 +583,6 @@ class PeerManagerImpl final : public PeerManager
583583
bool via_compact_block, const std::string& message = "")
584584
EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
585585

586-
/**
587-
* Potentially disconnect and discourage a node based on the contents of a TxValidationState object
588-
*/
589-
void MaybePunishNodeForTx(NodeId nodeid, const TxValidationState& state)
590-
EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
591-
592586
/** Maybe disconnect a peer and discourage future connections from its address.
593587
*
594588
* @param[in] pnode The node to check.
@@ -1836,32 +1830,6 @@ void PeerManagerImpl::MaybePunishNodeForBlock(NodeId nodeid, const BlockValidati
18361830
}
18371831
}
18381832

1839-
void PeerManagerImpl::MaybePunishNodeForTx(NodeId nodeid, const TxValidationState& state)
1840-
{
1841-
PeerRef peer{GetPeerRef(nodeid)};
1842-
switch (state.GetResult()) {
1843-
case TxValidationResult::TX_RESULT_UNSET:
1844-
break;
1845-
// The node is providing invalid data:
1846-
case TxValidationResult::TX_CONSENSUS:
1847-
if (peer) Misbehaving(*peer, "");
1848-
return;
1849-
// Conflicting (but not necessarily invalid) data or different policy:
1850-
case TxValidationResult::TX_INPUTS_NOT_STANDARD:
1851-
case TxValidationResult::TX_NOT_STANDARD:
1852-
case TxValidationResult::TX_MISSING_INPUTS:
1853-
case TxValidationResult::TX_PREMATURE_SPEND:
1854-
case TxValidationResult::TX_WITNESS_MUTATED:
1855-
case TxValidationResult::TX_WITNESS_STRIPPED:
1856-
case TxValidationResult::TX_CONFLICT:
1857-
case TxValidationResult::TX_MEMPOOL_POLICY:
1858-
case TxValidationResult::TX_NO_MEMPOOL:
1859-
case TxValidationResult::TX_RECONSIDERABLE:
1860-
case TxValidationResult::TX_UNKNOWN:
1861-
break;
1862-
}
1863-
}
1864-
18651833
bool PeerManagerImpl::BlockRequestAllowed(const CBlockIndex* pindex)
18661834
{
18671835
AssertLockHeld(cs_main);
@@ -3034,8 +3002,6 @@ std::optional<node::PackageToValidate> PeerManagerImpl::ProcessInvalidTx(NodeId
30343002
if (peer) AddKnownTx(*peer, parent_txid);
30353003
}
30363004

3037-
MaybePunishNodeForTx(nodeid, state);
3038-
30393005
return package_to_validate;
30403006
}
30413007

src/validation.cpp

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,34 +2191,17 @@ bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
21912191
if (pvChecks) {
21922192
pvChecks->emplace_back(std::move(check));
21932193
} else if (auto result = check(); result.has_value()) {
2194+
// Tx failures never trigger disconnections/bans.
2195+
// This is so that network splits aren't triggered
2196+
// either due to non-consensus relay policies (such as
2197+
// non-standard DER encodings or non-null dummy
2198+
// arguments) or due to new consensus rules introduced in
2199+
// soft forks.
21942200
if (flags & STANDARD_NOT_MANDATORY_VERIFY_FLAGS) {
2195-
// Check whether the failure was caused by a
2196-
// non-mandatory script verification check, such as
2197-
// non-standard DER encodings or non-null dummy
2198-
// arguments; if so, ensure we return NOT_STANDARD
2199-
// instead of CONSENSUS to avoid downstream users
2200-
// splitting the network between upgraded and
2201-
// non-upgraded nodes by banning CONSENSUS-failing
2202-
// data providers.
2203-
CScriptCheck check2(txdata.m_spent_outputs[i], tx, validation_cache.m_signature_cache, i,
2204-
flags & ~STANDARD_NOT_MANDATORY_VERIFY_FLAGS, cacheSigStore, &txdata);
2205-
auto mandatory_result = check2();
2206-
if (!mandatory_result.has_value()) {
2207-
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, strprintf("non-mandatory-script-verify-flag (%s)", ScriptErrorString(result->first)), result->second);
2208-
} else {
2209-
// If the second check failed, it failed due to a mandatory script verification
2210-
// flag, but the first check might have failed on a non-mandatory script
2211-
// verification flag.
2212-
//
2213-
// Avoid reporting a mandatory script check failure with a non-mandatory error
2214-
// string by reporting the error from the second check.
2215-
result = mandatory_result;
2216-
}
2201+
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, strprintf("mempool-script-verify-flag-failed (%s)", ScriptErrorString(result->first)), result->second);
2202+
} else {
2203+
return state.Invalid(TxValidationResult::TX_CONSENSUS, strprintf("mandatory-script-verify-flag-failed (%s)", ScriptErrorString(result->first)), result->second);
22172204
}
2218-
2219-
// MANDATORY flag failures correspond to
2220-
// TxValidationResult::TX_CONSENSUS.
2221-
return state.Invalid(TxValidationResult::TX_CONSENSUS, strprintf("mandatory-script-verify-flag-failed (%s)", ScriptErrorString(result->first)), result->second);
22222205
}
22232206
}
22242207

test/functional/data/invalid_txs.py

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,6 @@ class BadTxTemplate:
7373
# Only specified if it differs from mempool acceptance error.
7474
block_reject_reason = ""
7575

76-
# Do we expect to be disconnected after submitting this tx?
77-
expect_disconnect = False
78-
7976
# Is this tx considered valid when included in a block, but not for acceptance into
8077
# the mempool (i.e. does it violate policy but not consensus)?
8178
valid_in_block = False
@@ -93,7 +90,6 @@ def get_tx(self, *args, **kwargs):
9390

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

9894
def get_tx(self):
9995
tx = CTransaction()
@@ -103,7 +99,6 @@ def get_tx(self):
10399

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

108103
# We use a blank transaction here to make sure
109104
# it is interpreted as a non-witness transaction.
@@ -119,7 +114,6 @@ def get_tx(self):
119114
# tree depth commitment (CVE-2017-12842)
120115
class SizeTooSmall(BadTxTemplate):
121116
reject_reason = "tx-size-small"
122-
expect_disconnect = False
123117
valid_in_block = True
124118

125119
def get_tx(self):
@@ -137,7 +131,6 @@ class BadInputOutpointIndex(BadTxTemplate):
137131
reject_reason = None
138132
# But fails in block
139133
block_reject_reason = "bad-txns-inputs-missingorspent"
140-
expect_disconnect = False
141134

142135
def get_tx(self):
143136
num_indices = len(self.spend_tx.vin)
@@ -151,7 +144,6 @@ def get_tx(self):
151144

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

156148
def get_tx(self):
157149
tx = CTransaction()
@@ -163,7 +155,6 @@ def get_tx(self):
163155

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

168159
def get_tx(self):
169160
tx = CTransaction()
@@ -175,7 +166,6 @@ def get_tx(self):
175166

176167
class NonexistentInput(BadTxTemplate):
177168
reject_reason = None # Added as an orphan tx.
178-
expect_disconnect = False
179169
# But fails in block
180170
block_reject_reason = "bad-txns-inputs-missingorspent"
181171

@@ -189,7 +179,6 @@ def get_tx(self):
189179

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

194183
def get_tx(self):
195184
return create_tx_with_script(
@@ -198,23 +187,20 @@ def get_tx(self):
198187

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

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

206194

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

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

214201

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

219205
def get_tx(self):
220206
tx = create_tx_with_script(self.spend_tx, 0, amount=MAX_MONEY)
@@ -223,8 +209,7 @@ def get_tx(self):
223209

224210

225211
class InvalidOPIFConstruction(BadTxTemplate):
226-
reject_reason = "mandatory-script-verify-flag-failed (Invalid OP_IF construction)"
227-
expect_disconnect = True
212+
reject_reason = "mempool-script-verify-flag-failed (Invalid OP_IF construction)"
228213

229214
def get_tx(self):
230215
return create_tx_with_script(
@@ -235,7 +220,6 @@ def get_tx(self):
235220
class TooManySigopsPerBlock(BadTxTemplate):
236221
reject_reason = "bad-txns-too-many-sigops"
237222
block_reject_reason = "bad-blk-sigops, out-of-bounds SigOpCount"
238-
expect_disconnect = False
239223

240224
def get_tx(self):
241225
lotsa_checksigs = CScript([OP_CHECKSIG] * (MAX_BLOCK_SIGOPS))
@@ -247,7 +231,6 @@ def get_tx(self):
247231

248232
class TooManySigopsPerTransaction(BadTxTemplate):
249233
reject_reason = "bad-txns-too-many-sigops"
250-
expect_disconnect = False
251234
valid_in_block = True
252235

253236
def get_tx(self):
@@ -270,15 +253,14 @@ def get_tx(self):
270253

271254
return type('DisabledOpcode_' + str(opcode), (BadTxTemplate,), {
272255
'reject_reason': "disabled opcode",
273-
'expect_disconnect': True,
274256
'get_tx': get_tx,
275257
'valid_in_block' : False
276258
})
277259

278260
class NonStandardAndInvalid(BadTxTemplate):
279-
"""A non-standard transaction which is also consensus-invalid should return the consensus error."""
280-
reject_reason = "mandatory-script-verify-flag-failed (OP_RETURN was encountered)"
281-
expect_disconnect = True
261+
"""A non-standard transaction which is also consensus-invalid should return the first error."""
262+
reject_reason = "mempool-script-verify-flag-failed (Using OP_CODESEPARATOR in non-witness script)"
263+
block_reject_reason = "mandatory-script-verify-flag-failed (OP_RETURN was encountered)"
282264
valid_in_block = False
283265

284266
def get_tx(self):

test/functional/feature_block.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,6 @@ def run_test(self):
177177
for TxTemplate in invalid_txs.iter_all_templates():
178178
template = TxTemplate(spend_tx=attempt_spend_tx)
179179

180-
# belt-and-suspenders checking we won't pass up validating something
181-
# we expect a disconnect from
182-
if template.expect_disconnect:
183-
assert not template.valid_in_block
184-
185180
if template.valid_in_block:
186181
continue
187182

@@ -194,9 +189,12 @@ def run_test(self):
194189
if TxTemplate != invalid_txs.InputMissing:
195190
self.sign_tx(badtx, attempt_spend_tx)
196191
badblock = self.update_block(blockname, [badtx])
192+
reject_reason = (template.block_reject_reason or template.reject_reason)
193+
if reject_reason.startswith("mempool-script-verify-flag-failed"):
194+
reject_reason = "mandatory-script-verify-flag-failed" + reject_reason[33:]
197195
self.send_blocks(
198196
[badblock], success=False,
199-
reject_reason=(template.block_reject_reason or template.reject_reason),
197+
reject_reason=reject_reason,
200198
reconnect=True, timeout=2)
201199

202200
self.move_tip(2)

test/functional/feature_cltv.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,14 @@ def run_test(self):
153153
coin_vout = coin.prevout.n
154154
cltv_invalidate(spendtx, i)
155155

156+
blk_rej = "mandatory-script-verify-flag-failed"
157+
tx_rej = "mempool-script-verify-flag-failed"
156158
expected_cltv_reject_reason = [
157-
"mandatory-script-verify-flag-failed (Operation not valid with the current stack size)",
158-
"mandatory-script-verify-flag-failed (Negative locktime)",
159-
"mandatory-script-verify-flag-failed (Locktime requirement not satisfied)",
160-
"mandatory-script-verify-flag-failed (Locktime requirement not satisfied)",
161-
"mandatory-script-verify-flag-failed (Locktime requirement not satisfied)",
159+
" (Operation not valid with the current stack size)",
160+
" (Negative locktime)",
161+
" (Locktime requirement not satisfied)",
162+
" (Locktime requirement not satisfied)",
163+
" (Locktime requirement not satisfied)",
162164
][i]
163165
# First we show that this tx is valid except for CLTV by getting it
164166
# rejected from the mempool for exactly that reason.
@@ -169,8 +171,8 @@ def run_test(self):
169171
'txid': spendtx_txid,
170172
'wtxid': spendtx_wtxid,
171173
'allowed': False,
172-
'reject-reason': expected_cltv_reject_reason,
173-
'reject-details': expected_cltv_reject_reason + f", input 0 of {spendtx_txid} (wtxid {spendtx_wtxid}), spending {coin_txid}:{coin_vout}"
174+
'reject-reason': tx_rej + expected_cltv_reject_reason,
175+
'reject-details': tx_rej + expected_cltv_reject_reason + f", input 0 of {spendtx_txid} (wtxid {spendtx_wtxid}), spending {coin_txid}:{coin_vout}"
174176
}],
175177
self.nodes[0].testmempoolaccept(rawtxs=[spendtx.serialize().hex()], maxfeerate=0),
176178
)
@@ -180,7 +182,7 @@ def run_test(self):
180182
block.hashMerkleRoot = block.calc_merkle_root()
181183
block.solve()
182184

183-
with self.nodes[0].assert_debug_log(expected_msgs=[f'Block validation error: {expected_cltv_reject_reason}']):
185+
with self.nodes[0].assert_debug_log(expected_msgs=[f'Block validation error: {blk_rej + expected_cltv_reject_reason}']):
184186
peer.send_and_ping(msg_block(block))
185187
assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip)
186188
peer.sync_with_ping()

test/functional/feature_dersig.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ def run_test(self):
121121
'txid': spendtx_txid,
122122
'wtxid': spendtx_wtxid,
123123
'allowed': False,
124-
'reject-reason': 'mandatory-script-verify-flag-failed (Non-canonical DER signature)',
125-
'reject-details': 'mandatory-script-verify-flag-failed (Non-canonical DER signature), ' +
124+
'reject-reason': 'mempool-script-verify-flag-failed (Non-canonical DER signature)',
125+
'reject-details': 'mempool-script-verify-flag-failed (Non-canonical DER signature), ' +
126126
f"input 0 of {spendtx_txid} (wtxid {spendtx_wtxid}), spending {coin_txid}:0"
127127
}],
128128
self.nodes[0].testmempoolaccept(rawtxs=[spendtx.serialize().hex()], maxfeerate=0),

test/functional/feature_nulldummy.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@
3737
from test_framework.wallet import getnewdestination
3838
from test_framework.wallet_util import generate_keypair
3939

40-
NULLDUMMY_ERROR = "mandatory-script-verify-flag-failed (Dummy CHECKMULTISIG argument must be zero)"
41-
40+
NULLDUMMY_TX_ERROR = "mempool-script-verify-flag-failed (Dummy CHECKMULTISIG argument must be zero)"
41+
NULLDUMMY_BLK_ERROR = "mandatory-script-verify-flag-failed (Dummy CHECKMULTISIG argument must be zero)"
4242

4343
def invalidate_nulldummy_tx(tx):
4444
"""Transform a NULLDUMMY compliant tx (i.e. scriptSig starts with OP_0)
@@ -104,7 +104,7 @@ def run_test(self):
104104
addr=self.ms_address, amount=47,
105105
privkey=self.privkey)
106106
invalidate_nulldummy_tx(test2tx)
107-
assert_raises_rpc_error(-26, NULLDUMMY_ERROR, self.nodes[0].sendrawtransaction, test2tx.serialize_with_witness().hex(), 0)
107+
assert_raises_rpc_error(-26, NULLDUMMY_TX_ERROR, self.nodes[0].sendrawtransaction, test2tx.serialize_with_witness().hex(), 0)
108108

109109
self.log.info(f"Test 3: Non-NULLDUMMY base transactions should be accepted in a block before activation [{COINBASE_MATURITY + 4}]")
110110
self.block_submit(self.nodes[0], [test2tx], accept=True)
@@ -115,7 +115,7 @@ def run_test(self):
115115
privkey=self.privkey)
116116
test6txs = [CTransaction(test4tx)]
117117
invalidate_nulldummy_tx(test4tx)
118-
assert_raises_rpc_error(-26, NULLDUMMY_ERROR, self.nodes[0].sendrawtransaction, test4tx.serialize_with_witness().hex(), 0)
118+
assert_raises_rpc_error(-26, NULLDUMMY_TX_ERROR, self.nodes[0].sendrawtransaction, test4tx.serialize_with_witness().hex(), 0)
119119
self.block_submit(self.nodes[0], [test4tx], accept=False)
120120

121121
self.log.info("Test 5: Non-NULLDUMMY P2WSH multisig transaction invalid after activation")
@@ -125,7 +125,7 @@ def run_test(self):
125125
privkey=self.privkey)
126126
test6txs.append(CTransaction(test5tx))
127127
test5tx.wit.vtxinwit[0].scriptWitness.stack[0] = b'\x01'
128-
assert_raises_rpc_error(-26, NULLDUMMY_ERROR, self.nodes[0].sendrawtransaction, test5tx.serialize_with_witness().hex(), 0)
128+
assert_raises_rpc_error(-26, NULLDUMMY_TX_ERROR, self.nodes[0].sendrawtransaction, test5tx.serialize_with_witness().hex(), 0)
129129
self.block_submit(self.nodes[0], [test5tx], with_witness=True, accept=False)
130130

131131
self.log.info(f"Test 6: NULLDUMMY compliant base/witness transactions should be accepted to mempool and in block after activation [{COINBASE_MATURITY + 5}]")
@@ -141,7 +141,7 @@ def block_submit(self, node, txs, *, with_witness=False, accept):
141141
if with_witness:
142142
add_witness_commitment(block)
143143
block.solve()
144-
assert_equal(None if accept else NULLDUMMY_ERROR, node.submitblock(block.serialize().hex()))
144+
assert_equal(None if accept else NULLDUMMY_BLK_ERROR, node.submitblock(block.serialize().hex()))
145145
if accept:
146146
assert_equal(node.getbestblockhash(), block.hash_hex)
147147
self.lastblockhash = block.hash_hex

0 commit comments

Comments
 (0)