Skip to content

Commit b7702bd

Browse files
committed
Merge bitcoin/bitcoin#25943: rpc: Add a parameter to sendrawtransaction which sets a maximum value for unspendable outputs.
7013da0 Add release note for PR#25943 (David Gumberg) 04f270b Add test for unspendable transactions and parameter 'maxburnamount' to sendrawtransaction. (David Gumberg) Pull request description: This PR adds a user configurable, zero by default parameter — `maxburnamount` — to `sendrawtransaction`. This PR makes bitcoin core reject transactions that contain unspendable outputs which exceed `maxburnamount`. closes #25899. As a result of this PR, `sendrawtransaction` will by default block 3 kinds of transactions: 1. Those that begin with `OP_RETURN` - (datacarriers) 2. Those whose lengths exceed the script limit. 3. Those that contain invalid opcodes. The user is able to configure a `maxburnamount` that will override this check and allow a user to send a potentially unspendable output into the mempool. I see two legitimate use cases for this override: 1. Users that deliberately use `OP_RETURN` for datacarrier transactions that embed data into the blockchain. 2. Users that refuse to update, or are unable to update their bitcoin core client would be able to make use of new opcodes that their client doesn't know about. ACKs for top commit: glozow: reACK 7013da0 achow101: re-ACK 7013da0 Tree-SHA512: f786a796fb71a587d30313c96717fdf47e1106ab4ee0c16d713695e6c31ed6f6732dff6cbc91ca9841d66232166eb058f96028028e75c1507324426309ee4525
2 parents 32f9ce0 + 7013da0 commit b7702bd

File tree

7 files changed

+85
-3
lines changed

7 files changed

+85
-3
lines changed

doc/release-notes-25943.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
New RPC Argument
2+
--------
3+
- `sendrawtransaction` has a new, optional argument, `maxburnamount` with a default value of `0`. Any transaction containing an unspendable output with a value greater than `maxburnamount` will not be submitted. At present, the outputs deemed unspendable are those with scripts that begin with an `OP_RETURN` code (known as 'datacarriers'), scripts that exceed the maximum script size, and scripts that contain invalid opcodes.
4+

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
114114
{ "signrawtransactionwithkey", 2, "prevtxs" },
115115
{ "signrawtransactionwithwallet", 1, "prevtxs" },
116116
{ "sendrawtransaction", 1, "maxfeerate" },
117+
{ "sendrawtransaction", 2, "maxburnamount" },
117118
{ "testmempoolaccept", 0, "rawtxs" },
118119
{ "testmempoolaccept", 1, "maxfeerate" },
119120
{ "submitpackage", 0, "package" },

src/rpc/mempool.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <rpc/server.h>
1919
#include <rpc/server_util.h>
2020
#include <rpc/util.h>
21+
#include <script/standard.h>
2122
#include <txmempool.h>
2223
#include <univalue.h>
2324
#include <util/moneystr.h>
@@ -44,7 +45,11 @@ static RPCHelpMan sendrawtransaction()
4445
{"hexstring", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex string of the raw transaction"},
4546
{"maxfeerate", RPCArg::Type::AMOUNT, RPCArg::Default{FormatMoney(DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK())},
4647
"Reject transactions whose fee rate is higher than the specified value, expressed in " + CURRENCY_UNIT +
47-
"/kvB.\nSet to 0 to accept any fee rate.\n"},
48+
"/kvB.\nSet to 0 to accept any fee rate."},
49+
{"maxburnamount", RPCArg::Type::AMOUNT, RPCArg::Default{FormatMoney(0)},
50+
"Reject transactions with provably unspendable outputs (e.g. 'datacarrier' outputs that use the OP_RETURN opcode) greater than the specified value, expressed in " + CURRENCY_UNIT + ".\n"
51+
"If burning funds through unspendable outputs is desired, increase this value.\n"
52+
"This check is based on heuristics and does not guarantee spendability of outputs.\n"},
4853
},
4954
RPCResult{
5055
RPCResult::Type::STR_HEX, "", "The transaction hash in hex"
@@ -61,10 +66,19 @@ static RPCHelpMan sendrawtransaction()
6166
},
6267
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
6368
{
69+
const CAmount max_burn_amount = request.params[2].isNull() ? 0 : AmountFromValue(request.params[2]);
70+
6471
CMutableTransaction mtx;
6572
if (!DecodeHexTx(mtx, request.params[0].get_str())) {
6673
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed. Make sure the tx has at least one input.");
6774
}
75+
76+
for (const auto& out : mtx.vout) {
77+
if((out.scriptPubKey.IsUnspendable() || !out.scriptPubKey.HasValidOps()) && out.nValue > max_burn_amount) {
78+
throw JSONRPCTransactionError(TransactionError::MAX_BURN_EXCEEDED);
79+
}
80+
}
81+
6882
CTransactionRef tx(MakeTransactionRef(std::move(mtx)));
6983

7084
const CFeeRate max_raw_tx_fee_rate = request.params[1].isNull() ?

src/util/error.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ bilingual_str TransactionErrorString(const TransactionError err)
3333
return Untranslated("Specified sighash value does not match value stored in PSBT");
3434
case TransactionError::MAX_FEE_EXCEEDED:
3535
return Untranslated("Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)");
36+
case TransactionError::MAX_BURN_EXCEEDED:
37+
return Untranslated("Unspendable output exceeds maximum configured by user (maxburnamount)");
3638
case TransactionError::EXTERNAL_SIGNER_NOT_FOUND:
3739
return Untranslated("External signer not found");
3840
case TransactionError::EXTERNAL_SIGNER_FAILED:

src/util/error.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ enum class TransactionError {
3030
PSBT_MISMATCH,
3131
SIGHASH_MISMATCH,
3232
MAX_FEE_EXCEEDED,
33+
MAX_BURN_EXCEEDED,
3334
EXTERNAL_SIGNER_NOT_FOUND,
3435
EXTERNAL_SIGNER_FAILED,
3536
INVALID_PACKAGE,

test/functional/feature_coinstatsindex.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,10 @@ def _test_coin_stats_index(self):
156156

157157
# Generate and send another tx with an OP_RETURN output (which is unspendable)
158158
tx2 = self.wallet.create_self_transfer(utxo_to_spend=tx1_out_21)['tx']
159-
tx2.vout = [CTxOut(int(Decimal('20.99') * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30))]
159+
tx2_val = '20.99'
160+
tx2.vout = [CTxOut(int(Decimal(tx2_val) * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30))]
160161
tx2_hex = tx2.serialize().hex()
161-
self.nodes[0].sendrawtransaction(tx2_hex)
162+
self.nodes[0].sendrawtransaction(tx2_hex, 0, tx2_val)
162163

163164
# Include both txs in a block
164165
self.generate(self.nodes[0], 1)

test/functional/rpc_rawtransaction.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,17 @@
1818

1919
from test_framework.messages import (
2020
MAX_BIP125_RBF_SEQUENCE,
21+
COIN,
2122
CTransaction,
23+
CTxOut,
2224
tx_from_hex,
2325
)
26+
from test_framework.script import (
27+
CScript,
28+
OP_FALSE,
29+
OP_INVALIDOPCODE,
30+
OP_RETURN,
31+
)
2432
from test_framework.test_framework import BitcoinTestFramework
2533
from test_framework.util import (
2634
assert_equal,
@@ -331,6 +339,57 @@ def sendrawtransaction_tests(self):
331339
rawtx = self.nodes[2].createrawtransaction(inputs, outputs)
332340
assert_raises_rpc_error(-25, "bad-txns-inputs-missingorspent", self.nodes[2].sendrawtransaction, rawtx)
333341

342+
self.log.info("Test sendrawtransaction exceeding, falling short of, and equaling maxburnamount")
343+
max_burn_exceeded = "Unspendable output exceeds maximum configured by user (maxburnamount)"
344+
345+
346+
# Test that spendable transaction with default maxburnamount (0) gets sent
347+
tx = self.wallet.create_self_transfer()['tx']
348+
tx_hex = tx.serialize().hex()
349+
self.nodes[2].sendrawtransaction(hexstring=tx_hex)
350+
351+
# Test that datacarrier transaction with default maxburnamount (0) does not get sent
352+
tx = self.wallet.create_self_transfer()['tx']
353+
tx_val = 0.001
354+
tx.vout = [CTxOut(int(Decimal(tx_val) * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30))]
355+
tx_hex = tx.serialize().hex()
356+
assert_raises_rpc_error(-25, max_burn_exceeded, self.nodes[2].sendrawtransaction, tx_hex)
357+
358+
# Test that oversized script gets rejected by sendrawtransaction
359+
tx = self.wallet.create_self_transfer()['tx']
360+
tx_val = 0.001
361+
tx.vout = [CTxOut(int(Decimal(tx_val) * COIN), CScript([OP_FALSE] * 10001))]
362+
tx_hex = tx.serialize().hex()
363+
assert_raises_rpc_error(-25, max_burn_exceeded, self.nodes[2].sendrawtransaction, tx_hex)
364+
365+
# Test that script containing invalid opcode gets rejected by sendrawtransaction
366+
tx = self.wallet.create_self_transfer()['tx']
367+
tx_val = 0.01
368+
tx.vout = [CTxOut(int(Decimal(tx_val) * COIN), CScript([OP_INVALIDOPCODE]))]
369+
tx_hex = tx.serialize().hex()
370+
assert_raises_rpc_error(-25, max_burn_exceeded, self.nodes[2].sendrawtransaction, tx_hex)
371+
372+
# Test a transaction where our burn exceeds maxburnamount
373+
tx = self.wallet.create_self_transfer()['tx']
374+
tx_val = 0.001
375+
tx.vout = [CTxOut(int(Decimal(tx_val) * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30))]
376+
tx_hex = tx.serialize().hex()
377+
assert_raises_rpc_error(-25, max_burn_exceeded, self.nodes[2].sendrawtransaction, tx_hex, 0, 0.0009)
378+
379+
# Test a transaction where our burn falls short of maxburnamount
380+
tx = self.wallet.create_self_transfer()['tx']
381+
tx_val = 0.001
382+
tx.vout = [CTxOut(int(Decimal(tx_val) * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30))]
383+
tx_hex = tx.serialize().hex()
384+
self.nodes[2].sendrawtransaction(hexstring=tx_hex, maxfeerate='0', maxburnamount='0.0011')
385+
386+
# Test a transaction where our burn equals maxburnamount
387+
tx = self.wallet.create_self_transfer()['tx']
388+
tx_val = 0.001
389+
tx.vout = [CTxOut(int(Decimal(tx_val) * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30))]
390+
tx_hex = tx.serialize().hex()
391+
self.nodes[2].sendrawtransaction(hexstring=tx_hex, maxfeerate='0', maxburnamount='0.001')
392+
334393
def sendrawtransaction_testmempoolaccept_tests(self):
335394
self.log.info("Test sendrawtransaction/testmempoolaccept with maxfeerate")
336395
fee_exceeds_max = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)"

0 commit comments

Comments
 (0)