Skip to content

Commit 04f270b

Browse files
committed
Add test for unspendable transactions and parameter 'maxburnamount' to sendrawtransaction.
'maxburnamount' sets a maximum value for outputs heuristically deemed unspendable including datacarrier scripts that begin with `OP_RETURN`.
1 parent 8ae2808 commit 04f270b

File tree

6 files changed

+81
-3
lines changed

6 files changed

+81
-3
lines changed

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)