mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-19 05:45:05 +01:00
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`.
This commit is contained in:
parent
8ae2808a43
commit
04f270b435
@ -114,6 +114,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||||||
{ "signrawtransactionwithkey", 2, "prevtxs" },
|
{ "signrawtransactionwithkey", 2, "prevtxs" },
|
||||||
{ "signrawtransactionwithwallet", 1, "prevtxs" },
|
{ "signrawtransactionwithwallet", 1, "prevtxs" },
|
||||||
{ "sendrawtransaction", 1, "maxfeerate" },
|
{ "sendrawtransaction", 1, "maxfeerate" },
|
||||||
|
{ "sendrawtransaction", 2, "maxburnamount" },
|
||||||
{ "testmempoolaccept", 0, "rawtxs" },
|
{ "testmempoolaccept", 0, "rawtxs" },
|
||||||
{ "testmempoolaccept", 1, "maxfeerate" },
|
{ "testmempoolaccept", 1, "maxfeerate" },
|
||||||
{ "submitpackage", 0, "package" },
|
{ "submitpackage", 0, "package" },
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
#include <rpc/server.h>
|
#include <rpc/server.h>
|
||||||
#include <rpc/server_util.h>
|
#include <rpc/server_util.h>
|
||||||
#include <rpc/util.h>
|
#include <rpc/util.h>
|
||||||
|
#include <script/standard.h>
|
||||||
#include <txmempool.h>
|
#include <txmempool.h>
|
||||||
#include <univalue.h>
|
#include <univalue.h>
|
||||||
#include <util/moneystr.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"},
|
{"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())},
|
{"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 +
|
"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{
|
||||||
RPCResult::Type::STR_HEX, "", "The transaction hash in hex"
|
RPCResult::Type::STR_HEX, "", "The transaction hash in hex"
|
||||||
@ -61,10 +66,19 @@ static RPCHelpMan sendrawtransaction()
|
|||||||
},
|
},
|
||||||
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
||||||
{
|
{
|
||||||
|
const CAmount max_burn_amount = request.params[2].isNull() ? 0 : AmountFromValue(request.params[2]);
|
||||||
|
|
||||||
CMutableTransaction mtx;
|
CMutableTransaction mtx;
|
||||||
if (!DecodeHexTx(mtx, request.params[0].get_str())) {
|
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.");
|
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)));
|
CTransactionRef tx(MakeTransactionRef(std::move(mtx)));
|
||||||
|
|
||||||
const CFeeRate max_raw_tx_fee_rate = request.params[1].isNull() ?
|
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");
|
return Untranslated("Specified sighash value does not match value stored in PSBT");
|
||||||
case TransactionError::MAX_FEE_EXCEEDED:
|
case TransactionError::MAX_FEE_EXCEEDED:
|
||||||
return Untranslated("Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)");
|
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:
|
case TransactionError::EXTERNAL_SIGNER_NOT_FOUND:
|
||||||
return Untranslated("External signer not found");
|
return Untranslated("External signer not found");
|
||||||
case TransactionError::EXTERNAL_SIGNER_FAILED:
|
case TransactionError::EXTERNAL_SIGNER_FAILED:
|
||||||
|
@ -30,6 +30,7 @@ enum class TransactionError {
|
|||||||
PSBT_MISMATCH,
|
PSBT_MISMATCH,
|
||||||
SIGHASH_MISMATCH,
|
SIGHASH_MISMATCH,
|
||||||
MAX_FEE_EXCEEDED,
|
MAX_FEE_EXCEEDED,
|
||||||
|
MAX_BURN_EXCEEDED,
|
||||||
EXTERNAL_SIGNER_NOT_FOUND,
|
EXTERNAL_SIGNER_NOT_FOUND,
|
||||||
EXTERNAL_SIGNER_FAILED,
|
EXTERNAL_SIGNER_FAILED,
|
||||||
INVALID_PACKAGE,
|
INVALID_PACKAGE,
|
||||||
|
@ -156,9 +156,10 @@ class CoinStatsIndexTest(BitcoinTestFramework):
|
|||||||
|
|
||||||
# Generate and send another tx with an OP_RETURN output (which is unspendable)
|
# 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 = 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()
|
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
|
# Include both txs in a block
|
||||||
self.generate(self.nodes[0], 1)
|
self.generate(self.nodes[0], 1)
|
||||||
|
@ -18,9 +18,17 @@ from itertools import product
|
|||||||
|
|
||||||
from test_framework.messages import (
|
from test_framework.messages import (
|
||||||
MAX_BIP125_RBF_SEQUENCE,
|
MAX_BIP125_RBF_SEQUENCE,
|
||||||
|
COIN,
|
||||||
CTransaction,
|
CTransaction,
|
||||||
|
CTxOut,
|
||||||
tx_from_hex,
|
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.test_framework import BitcoinTestFramework
|
||||||
from test_framework.util import (
|
from test_framework.util import (
|
||||||
assert_equal,
|
assert_equal,
|
||||||
@ -331,6 +339,57 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||||||
rawtx = self.nodes[2].createrawtransaction(inputs, outputs)
|
rawtx = self.nodes[2].createrawtransaction(inputs, outputs)
|
||||||
assert_raises_rpc_error(-25, "bad-txns-inputs-missingorspent", self.nodes[2].sendrawtransaction, rawtx)
|
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):
|
def sendrawtransaction_testmempoolaccept_tests(self):
|
||||||
self.log.info("Test sendrawtransaction/testmempoolaccept with maxfeerate")
|
self.log.info("Test sendrawtransaction/testmempoolaccept with maxfeerate")
|
||||||
fee_exceeds_max = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)"
|
fee_exceeds_max = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)"
|
||||||
|
Loading…
Reference in New Issue
Block a user