mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-02-08 10:31:50 -05:00
Merge bitcoin/bitcoin#25943: rpc: Add a parameter to sendrawtransaction which sets a maximum value for unspendable outputs.
7013da07fb
Add release note for PR#25943 (David Gumberg)04f270b435
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: reACK7013da07fb
achow101: re-ACK7013da07fb
Tree-SHA512: f786a796fb71a587d30313c96717fdf47e1106ab4ee0c16d713695e6c31ed6f6732dff6cbc91ca9841d66232166eb058f96028028e75c1507324426309ee4525
This commit is contained in:
commit
b7702bd546
7 changed files with 85 additions and 3 deletions
4
doc/release-notes-25943.md
Normal file
4
doc/release-notes-25943.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
New RPC Argument
|
||||
--------
|
||||
- `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.
|
||||
|
|
@ -114,6 +114,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||
{ "signrawtransactionwithkey", 2, "prevtxs" },
|
||||
{ "signrawtransactionwithwallet", 1, "prevtxs" },
|
||||
{ "sendrawtransaction", 1, "maxfeerate" },
|
||||
{ "sendrawtransaction", 2, "maxburnamount" },
|
||||
{ "testmempoolaccept", 0, "rawtxs" },
|
||||
{ "testmempoolaccept", 1, "maxfeerate" },
|
||||
{ "submitpackage", 0, "package" },
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#include <rpc/server.h>
|
||||
#include <rpc/server_util.h>
|
||||
#include <rpc/util.h>
|
||||
#include <script/standard.h>
|
||||
#include <txmempool.h>
|
||||
#include <univalue.h>
|
||||
#include <util/moneystr.h>
|
||||
|
@ -44,7 +45,11 @@ static RPCHelpMan sendrawtransaction()
|
|||
{"hexstring", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex string of the raw transaction"},
|
||||
{"maxfeerate", RPCArg::Type::AMOUNT, RPCArg::Default{FormatMoney(DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK())},
|
||||
"Reject transactions whose fee rate is higher than the specified value, expressed in " + CURRENCY_UNIT +
|
||||
"/kvB.\nSet to 0 to accept any fee rate.\n"},
|
||||
"/kvB.\nSet to 0 to accept any fee rate."},
|
||||
{"maxburnamount", RPCArg::Type::AMOUNT, RPCArg::Default{FormatMoney(0)},
|
||||
"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"
|
||||
"If burning funds through unspendable outputs is desired, increase this value.\n"
|
||||
"This check is based on heuristics and does not guarantee spendability of outputs.\n"},
|
||||
},
|
||||
RPCResult{
|
||||
RPCResult::Type::STR_HEX, "", "The transaction hash in hex"
|
||||
|
@ -61,10 +66,19 @@ static RPCHelpMan sendrawtransaction()
|
|||
},
|
||||
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
||||
{
|
||||
const CAmount max_burn_amount = request.params[2].isNull() ? 0 : AmountFromValue(request.params[2]);
|
||||
|
||||
CMutableTransaction mtx;
|
||||
if (!DecodeHexTx(mtx, request.params[0].get_str())) {
|
||||
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed. Make sure the tx has at least one input.");
|
||||
}
|
||||
|
||||
for (const auto& out : mtx.vout) {
|
||||
if((out.scriptPubKey.IsUnspendable() || !out.scriptPubKey.HasValidOps()) && out.nValue > max_burn_amount) {
|
||||
throw JSONRPCTransactionError(TransactionError::MAX_BURN_EXCEEDED);
|
||||
}
|
||||
}
|
||||
|
||||
CTransactionRef tx(MakeTransactionRef(std::move(mtx)));
|
||||
|
||||
const CFeeRate max_raw_tx_fee_rate = request.params[1].isNull() ?
|
||||
|
|
|
@ -33,6 +33,8 @@ bilingual_str TransactionErrorString(const TransactionError err)
|
|||
return Untranslated("Specified sighash value does not match value stored in PSBT");
|
||||
case TransactionError::MAX_FEE_EXCEEDED:
|
||||
return Untranslated("Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)");
|
||||
case TransactionError::MAX_BURN_EXCEEDED:
|
||||
return Untranslated("Unspendable output exceeds maximum configured by user (maxburnamount)");
|
||||
case TransactionError::EXTERNAL_SIGNER_NOT_FOUND:
|
||||
return Untranslated("External signer not found");
|
||||
case TransactionError::EXTERNAL_SIGNER_FAILED:
|
||||
|
|
|
@ -30,6 +30,7 @@ enum class TransactionError {
|
|||
PSBT_MISMATCH,
|
||||
SIGHASH_MISMATCH,
|
||||
MAX_FEE_EXCEEDED,
|
||||
MAX_BURN_EXCEEDED,
|
||||
EXTERNAL_SIGNER_NOT_FOUND,
|
||||
EXTERNAL_SIGNER_FAILED,
|
||||
INVALID_PACKAGE,
|
||||
|
|
|
@ -156,9 +156,10 @@ class CoinStatsIndexTest(BitcoinTestFramework):
|
|||
|
||||
# Generate and send another tx with an OP_RETURN output (which is unspendable)
|
||||
tx2 = self.wallet.create_self_transfer(utxo_to_spend=tx1_out_21)['tx']
|
||||
tx2.vout = [CTxOut(int(Decimal('20.99') * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30))]
|
||||
tx2_val = '20.99'
|
||||
tx2.vout = [CTxOut(int(Decimal(tx2_val) * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30))]
|
||||
tx2_hex = tx2.serialize().hex()
|
||||
self.nodes[0].sendrawtransaction(tx2_hex)
|
||||
self.nodes[0].sendrawtransaction(tx2_hex, 0, tx2_val)
|
||||
|
||||
# Include both txs in a block
|
||||
self.generate(self.nodes[0], 1)
|
||||
|
|
|
@ -18,9 +18,17 @@ from itertools import product
|
|||
|
||||
from test_framework.messages import (
|
||||
MAX_BIP125_RBF_SEQUENCE,
|
||||
COIN,
|
||||
CTransaction,
|
||||
CTxOut,
|
||||
tx_from_hex,
|
||||
)
|
||||
from test_framework.script import (
|
||||
CScript,
|
||||
OP_FALSE,
|
||||
OP_INVALIDOPCODE,
|
||||
OP_RETURN,
|
||||
)
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
|
@ -331,6 +339,57 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||
rawtx = self.nodes[2].createrawtransaction(inputs, outputs)
|
||||
assert_raises_rpc_error(-25, "bad-txns-inputs-missingorspent", self.nodes[2].sendrawtransaction, rawtx)
|
||||
|
||||
self.log.info("Test sendrawtransaction exceeding, falling short of, and equaling maxburnamount")
|
||||
max_burn_exceeded = "Unspendable output exceeds maximum configured by user (maxburnamount)"
|
||||
|
||||
|
||||
# Test that spendable transaction with default maxburnamount (0) gets sent
|
||||
tx = self.wallet.create_self_transfer()['tx']
|
||||
tx_hex = tx.serialize().hex()
|
||||
self.nodes[2].sendrawtransaction(hexstring=tx_hex)
|
||||
|
||||
# Test that datacarrier transaction with default maxburnamount (0) does not get sent
|
||||
tx = self.wallet.create_self_transfer()['tx']
|
||||
tx_val = 0.001
|
||||
tx.vout = [CTxOut(int(Decimal(tx_val) * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30))]
|
||||
tx_hex = tx.serialize().hex()
|
||||
assert_raises_rpc_error(-25, max_burn_exceeded, self.nodes[2].sendrawtransaction, tx_hex)
|
||||
|
||||
# Test that oversized script gets rejected by sendrawtransaction
|
||||
tx = self.wallet.create_self_transfer()['tx']
|
||||
tx_val = 0.001
|
||||
tx.vout = [CTxOut(int(Decimal(tx_val) * COIN), CScript([OP_FALSE] * 10001))]
|
||||
tx_hex = tx.serialize().hex()
|
||||
assert_raises_rpc_error(-25, max_burn_exceeded, self.nodes[2].sendrawtransaction, tx_hex)
|
||||
|
||||
# Test that script containing invalid opcode gets rejected by sendrawtransaction
|
||||
tx = self.wallet.create_self_transfer()['tx']
|
||||
tx_val = 0.01
|
||||
tx.vout = [CTxOut(int(Decimal(tx_val) * COIN), CScript([OP_INVALIDOPCODE]))]
|
||||
tx_hex = tx.serialize().hex()
|
||||
assert_raises_rpc_error(-25, max_burn_exceeded, self.nodes[2].sendrawtransaction, tx_hex)
|
||||
|
||||
# Test a transaction where our burn exceeds maxburnamount
|
||||
tx = self.wallet.create_self_transfer()['tx']
|
||||
tx_val = 0.001
|
||||
tx.vout = [CTxOut(int(Decimal(tx_val) * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30))]
|
||||
tx_hex = tx.serialize().hex()
|
||||
assert_raises_rpc_error(-25, max_burn_exceeded, self.nodes[2].sendrawtransaction, tx_hex, 0, 0.0009)
|
||||
|
||||
# Test a transaction where our burn falls short of maxburnamount
|
||||
tx = self.wallet.create_self_transfer()['tx']
|
||||
tx_val = 0.001
|
||||
tx.vout = [CTxOut(int(Decimal(tx_val) * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30))]
|
||||
tx_hex = tx.serialize().hex()
|
||||
self.nodes[2].sendrawtransaction(hexstring=tx_hex, maxfeerate='0', maxburnamount='0.0011')
|
||||
|
||||
# Test a transaction where our burn equals maxburnamount
|
||||
tx = self.wallet.create_self_transfer()['tx']
|
||||
tx_val = 0.001
|
||||
tx.vout = [CTxOut(int(Decimal(tx_val) * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30))]
|
||||
tx_hex = tx.serialize().hex()
|
||||
self.nodes[2].sendrawtransaction(hexstring=tx_hex, maxfeerate='0', maxburnamount='0.001')
|
||||
|
||||
def sendrawtransaction_testmempoolaccept_tests(self):
|
||||
self.log.info("Test sendrawtransaction/testmempoolaccept with maxfeerate")
|
||||
fee_exceeds_max = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)"
|
||||
|
|
Loading…
Add table
Reference in a new issue