Skip to content

Commit

Permalink
Merge bitcoin#13541: wallet/rpc: sendrawtransaction maxfeerate
Browse files Browse the repository at this point in the history
7abd2e6 wallet/rpc: add maxfeerate parameter to testmempoolaccept (Karl-Johan Alm)
6c0a6f7 wallet/rpc: add maxfeerate parameter to sendrawtransaction (Karl-Johan Alm)
e5efacb test: Refactor vout fetches in rpc_rawtransaction (Karl-Johan Alm)

Pull request description:

  This adds a new `maxfeerate` parameter to `sendrawtransaction` which forces the node to reject a transaction whose feerate is above the given fee rate.

  This is a safety harness from shooting yourself in the foot and accidentally overpaying fees.

  See also bitcoin#12911.

Tree-SHA512: efa50134a7c17c9330cfdfd48ba400e095c0a419cc45e630618d8b44929c25d780d1bb2710c1fbbb6e687eca373505b0338cdaa7f2ff4ca22636d84c31557a2e
  • Loading branch information
MarcoFalke authored and vijaydasmp committed Nov 16, 2021
1 parent 73c3648 commit 99d82a8
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 39 deletions.
2 changes: 2 additions & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,10 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "sendrawtransaction", 1, "allowhighfees" },
{ "sendrawtransaction", 2, "instantsend" },
{ "sendrawtransaction", 3, "bypasslimits" },
{ "sendrawtransaction", 1, "maxfeerate" },
{ "testmempoolaccept", 0, "rawtxs" },
{ "testmempoolaccept", 1, "allowhighfees" },
{ "testmempoolaccept", 1, "maxfeerate" },
{ "combinerawtransaction", 0, "txs" },
{ "fundrawtransaction", 1, "options" },
{ "walletcreatefundedpsbt", 0, "inputs" },
Expand Down
57 changes: 44 additions & 13 deletions src/rpc/rawtransaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include <txmempool.h>
#include <uint256.h>
#include <util/validation.h>
#include <util/moneystr.h>
#include <util/strencodings.h>
#include <validation.h>
#include <validationinterface.h>
Expand Down Expand Up @@ -758,7 +759,7 @@ UniValue sendrawtransaction(const JSONRPCRequest& request)
"\nAlso see createrawtransaction and signrawtransactionwithkey calls.\n",
{
{"hexstring", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex string of the raw transaction"},
{"allowhighfees", RPCArg::Type::BOOL, /* default */ "false", "Allow high fees"},
{"maxfeerate", RPCArg::Type::AMOUNT, /* default */FormatMoney(maxTxFee), "Reject transactions whose fee rate is higher than the specified value, expressed in " + CURRENCY_UNIT + "/kB\n"},
{"instantsend", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Deprecated and ignored"},
{"bypasslimits", RPCArg::Type::BOOL, /* default_val */ "false", "Bypass transaction policy limits"},
},
Expand All @@ -777,21 +778,37 @@ UniValue sendrawtransaction(const JSONRPCRequest& request)
},
}.ToString());

RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VBOOL, UniValue::VBOOL});
RPCTypeCheck(request.params, {
UniValue::VSTR,
UniValueType(), // NUM or BOOL, checked later
});

// parse hex string from parameter
CMutableTransaction mtx;
if (!DecodeHexTx(mtx, request.params[0].get_str()))
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed");
CTransactionRef tx(MakeTransactionRef(std::move(mtx)));
bool allowhighfees = false;
if (!request.params[1].isNull()) allowhighfees = request.params[1].get_bool();
bool bypass_limits = false;
if (!request.params[3].isNull()) bypass_limits = request.params[3].get_bool();
const CAmount highfee{allowhighfees ? 0 : ::maxTxFee};

CAmount max_raw_tx_fee = maxTxFee;
// TODO: temporary migration code for old clients. Remove in v0.20
if (request.params[1].isBool()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Second argument must be numeric (maxfeerate) and no longer supports a boolean. To allow a transaction with high fees, set maxfeerate to 0.");
} else if (request.params[1].isNum()) {
size_t weight = GetTransactionWeight(*tx);
CFeeRate fr(AmountFromValue(request.params[1]));
// the +3/4 part rounds the value up, and is the same formula used when
// calculating the fee for a transaction
// (see GetVirtualTransactionSize)
max_raw_tx_fee = fr.GetFee((weight+3)/4);
} else if (!request.params[1].isNull()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "second argument (maxfeerate) must be numeric");
}

uint256 txid;
std::string err_string;
const TransactionError err = BroadcastTransaction(tx, txid, err_string, highfee, bypass_limits);
const TransactionError err = BroadcastTransaction(tx, txid, err_string, max_raw_tx_fee);
if (TransactionError::OK != err) {
throw JSONRPCTransactionError(err, err_string);
}
Expand All @@ -814,7 +831,7 @@ static UniValue testmempoolaccept(const JSONRPCRequest& request)
{"rawtx", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, ""},
},
},
{"allowhighfees", RPCArg::Type::BOOL, /* default */ "false", "Allow high fees"},
{"maxfeerate", RPCArg::Type::AMOUNT, /* default */ FormatMoney(maxTxFee), "Reject transactions whose fee rate is higher than the specified value, expressed in " + CURRENCY_UNIT + "/kB\n"},
},
RPCResult{
"[ (array) The result of the mempool acceptance test for each raw transaction in the input array.\n"
Expand All @@ -839,7 +856,11 @@ static UniValue testmempoolaccept(const JSONRPCRequest& request)
}.ToString());
}

RPCTypeCheck(request.params, {UniValue::VARR, UniValue::VBOOL});
RPCTypeCheck(request.params, {
UniValue::VARR,
UniValueType(), // NUM or BOOL, checked later
});

if (request.params[0].get_array().size() != 1) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Array must contain exactly one raw transaction for now");
}
Expand All @@ -851,9 +872,19 @@ static UniValue testmempoolaccept(const JSONRPCRequest& request)
CTransactionRef tx(MakeTransactionRef(std::move(mtx)));
const uint256& tx_hash = tx->GetHash();

CAmount max_raw_tx_fee = ::maxTxFee;
if (!request.params[1].isNull() && request.params[1].get_bool()) {
max_raw_tx_fee = 0;
CAmount max_raw_tx_fee = maxTxFee;
// TODO: temporary migration code for old clients. Remove in v0.20
if (request.params[1].isBool()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Second argument must be numeric (maxfeerate) and no longer supports a boolean. To allow a transaction with high fees, set maxfeerate to 0.");
} else if (request.params[1].isNum()) {
size_t weight = GetTransactionWeight(*tx);
CFeeRate fr(AmountFromValue(request.params[1]));
// the +3/4 part rounds the value up, and is the same formula used when
// calculating the fee for a transaction
// (see GetVirtualTransactionSize)
max_raw_tx_fee = fr.GetFee((weight+3)/4);
} else if (!request.params[1].isNull()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "second argument (maxfeerate) must be numeric");
}

UniValue result(UniValue::VARR);
Expand Down Expand Up @@ -1375,10 +1406,10 @@ static const CRPCCommand commands[] =
{ "rawtransactions", "createrawtransaction", &createrawtransaction, {"inputs","outputs","locktime"} },
{ "rawtransactions", "decoderawtransaction", &decoderawtransaction, {"hexstring"} },
{ "rawtransactions", "decodescript", &decodescript, {"hexstring"} },
{ "rawtransactions", "sendrawtransaction", &sendrawtransaction, {"hexstring","allowhighfees","instantsend","bypasslimits"} },
{ "rawtransactions", "sendrawtransaction", &sendrawtransaction, {"hexstring","allowhighfees|maxfeerate","instantsend","bypasslimits"} },
{ "rawtransactions", "combinerawtransaction", &combinerawtransaction, {"txs"} },
{ "rawtransactions", "signrawtransactionwithkey", &signrawtransactionwithkey, {"hexstring","privkeys","prevtxs","sighashtype"} },
{ "rawtransactions", "testmempoolaccept", &testmempoolaccept, {"rawtxs","allowhighfees"} },
{ "rawtransactions", "testmempoolaccept", &testmempoolaccept, {"rawtxs","allowhighfees|maxfeerate"} },
{ "rawtransactions", "decodepsbt", &decodepsbt, {"psbt"} },
{ "rawtransactions", "combinepsbt", &combinepsbt, {"txs"} },
{ "rawtransactions", "finalizepsbt", &finalizepsbt, {"psbt", "extract"} },
Expand Down
20 changes: 20 additions & 0 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3773,6 +3773,26 @@ static CBlockIndex* GetLastCheckpoint(const CCheckpointData& data) EXCLUSIVE_LOC
return nullptr;
}

// Compute at which vout of the block's coinbase transaction the signet
// signature occurs, or -1 if not found.
static int GetSignetSignatureIndex(const CBlock& block)
{
if (!block.vtx.empty()) {
for (size_t o = 0; o < block.vtx[0]->vout.size(); o++) {
if (block.vtx[0]->vout[o].scriptPubKey.size() >= 68 // at minimum 64 byte signature plus script/header data
&& block.vtx[0]->vout[o].scriptPubKey[0] == OP_RETURN // unspendable
&& block.vtx[0]->vout[o].scriptPubKey[1] == block.vtx[0]->vout[o].scriptPubKey.size() - 1 // push the rest
&& block.vtx[0]->vout[o].scriptPubKey[2] == 0xec // 's' ^ 0x9f
&& block.vtx[0]->vout[o].scriptPubKey[3] == 0xc7 // 'i' ^ 0xae
&& block.vtx[0]->vout[o].scriptPubKey[4] == 0xda // 'g' ^ 0xbd
&& block.vtx[0]->vout[o].scriptPubKey[5] == 0xa2) { // 'n' ^ 0xcc
return (int)o;
}
}
}
return -1;
}

/** Context-dependent validity checks.
* By "context", we mean only the previous block headers, but not the UTXO
* set; UTXO-related validity checks are done in ConnectBlock().
Expand Down
4 changes: 2 additions & 2 deletions test/functional/feature_fee_estimation.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def small_txpuzzle_randfee(from_node, conflist, unconflist, amount, min_fee, fee
# the ScriptSig that will satisfy the ScriptPubKey.
for inp in tx.vin:
inp.scriptSig = SCRIPT_SIG[inp.prevout.n]
txid = from_node.sendrawtransaction(ToHex(tx), True)
txid = from_node.sendrawtransaction(hexstring=ToHex(tx), maxfeerate=0)
unconflist.append({"txid": txid, "vout": 0, "amount": total_in - amount - fee})
unconflist.append({"txid": txid, "vout": 1, "amount": amount})

Expand Down Expand Up @@ -93,7 +93,7 @@ def split_inputs(from_node, txins, txouts, initial_split=False):
else:
tx.vin[0].scriptSig = SCRIPT_SIG[prevtxout["vout"]]
completetx = ToHex(tx)
txid = from_node.sendrawtransaction(completetx, True)
txid = from_node.sendrawtransaction(hexstring=completetx, maxfeerate=0)
txouts.append({"txid": txid, "vout": 0, "amount": half_change})
txouts.append({"txid": txid, "vout": 1, "amount": rem_change})

Expand Down
10 changes: 5 additions & 5 deletions test/functional/feature_nulldummy.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,15 @@ def run_test(self):

self.log.info("Test 1: NULLDUMMY compliant base transactions should be accepted to mempool and mined before activation [430]")
test1txs = [create_transaction(self.nodes[0], coinbase_txid[0], self.ms_address, 49)]
txid1 = self.nodes[0].sendrawtransaction(test1txs[0].serialize().hex(), True)
txid1 = self.nodes[0].sendrawtransaction(test1txs[0].serialize().hex(), 0)
test1txs.append(create_transaction(self.nodes[0], txid1, self.ms_address, 48))
txid2 = self.nodes[0].sendrawtransaction(test1txs[1].serialize().hex(), True)
txid2 = self.nodes[0].sendrawtransaction(test1txs[1].serialize().hex(), 0)
self.block_submit(self.nodes[0], test1txs, True)

self.log.info("Test 2: Non-NULLDUMMY base multisig transaction should not be accepted to mempool before activation")
test2tx = create_transaction(self.nodes[0], txid2, self.ms_address, 47)
trueDummy(test2tx)
assert_raises_rpc_error(-26, NULLDUMMY_ERROR, self.nodes[0].sendrawtransaction, test2tx.serialize().hex(), True)
assert_raises_rpc_error(-26, NULLDUMMY_ERROR, self.nodes[0].sendrawtransaction, test2tx.serialize_with_witness().hex(), 0)

self.log.info("Test 3: Non-NULLDUMMY base transactions should be accepted in a block before activation [431]")
self.block_submit(self.nodes[0], [test2tx], True)
Expand All @@ -77,12 +77,12 @@ def run_test(self):
test4tx = create_transaction(self.nodes[0], test2tx.hash, self.address, 46)
test6txs=[CTransaction(test4tx)]
trueDummy(test4tx)
assert_raises_rpc_error(-26, NULLDUMMY_ERROR, self.nodes[0].sendrawtransaction, test4tx.serialize().hex(), True)
assert_raises_rpc_error(-26, NULLDUMMY_ERROR, self.nodes[0].sendrawtransaction, test4tx.serialize().hex(), 0)
self.block_submit(self.nodes[0], [test4tx])

self.log.info("Test 6: NULLDUMMY compliant transactions should be accepted to mempool and in block after activation [432]")
for i in test6txs:
self.nodes[0].sendrawtransaction(i.serialize().hex(), True)
self.nodes[0].sendrawtransaction(i.serialize().hex(), 0)
self.block_submit(self.nodes[0], test6txs, True)


Expand Down
14 changes: 7 additions & 7 deletions test/functional/mempool_accept.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def run_test(self):
inputs=[{'txid': coin['txid'], 'vout': coin['vout']}],
outputs=[{node.getnewaddress(): 0.3}, {node.getnewaddress(): 49}],
))['hex']
txid_in_block = node.sendrawtransaction(hexstring=raw_tx_in_block, allowhighfees=True)
txid_in_block = node.sendrawtransaction(hexstring=raw_tx_in_block, maxfeerate=0)
node.generate(1)
self.mempool_size = 0
self.check_mempool_result(
Expand Down Expand Up @@ -103,9 +103,9 @@ def run_test(self):
self.check_mempool_result(
result_expected=[{'txid': tx.rehash(), 'allowed': True}],
rawtxs=[tx.serialize().hex()],
allowhighfees=True,
maxfeerate=0,
)
node.sendrawtransaction(hexstring=raw_tx_final, allowhighfees=True)
node.sendrawtransaction(hexstring=raw_tx_final, maxfeerate=0)
self.mempool_size += 1

self.log.info('A transaction in the mempool')
Expand Down Expand Up @@ -139,7 +139,7 @@ def run_test(self):
self.check_mempool_result(
result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '18: txn-mempool-conflict'}],
rawtxs=[tx.serialize().hex()],
allowhighfees=True,
maxfeerate=0,
)

self.log.info('A transaction with missing inputs, that never existed')
Expand All @@ -155,7 +155,7 @@ def run_test(self):
tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_0)))
tx.vin[0].prevout.n = 1 # Set vout to 1, to spend the other outpoint (49 coins) of the in-chain-tx we want to double spend
raw_tx_1 = node.signrawtransactionwithwallet(tx.serialize().hex())['hex']
txid_1 = node.sendrawtransaction(hexstring=raw_tx_1, allowhighfees=True)
txid_1 = node.sendrawtransaction(hexstring=raw_tx_1, maxfeerate=0)
# Now spend both to "clearly hide" the outputs, ie. remove the coins from the utxo set by spending them
raw_tx_spend_both = node.signrawtransactionwithwallet(node.createrawtransaction(
inputs=[
Expand All @@ -164,7 +164,7 @@ def run_test(self):
],
outputs=[{node.getnewaddress(): 0.1}]
))['hex']
txid_spend_both = node.sendrawtransaction(hexstring=raw_tx_spend_both, allowhighfees=True)
txid_spend_both = node.sendrawtransaction(hexstring=raw_tx_spend_both, maxfeerate=0)
node.generate(1)
self.mempool_size = 0
# Now see if we can add the coins back to the utxo set by sending the exact txs again
Expand Down Expand Up @@ -313,7 +313,7 @@ def run_test(self):
self.check_mempool_result(
result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '64: non-BIP68-final'}],
rawtxs=[tx.serialize().hex()],
allowhighfees=True,
maxfeerate=0,
)


Expand Down
37 changes: 27 additions & 10 deletions test/functional/rpc_rawtransaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,7 @@ def run_test(self):

txDetails = self.nodes[0].gettransaction(txId, True)
rawTx = self.nodes[0].decoderawtransaction(txDetails['hex'])
vout = False
for outpoint in rawTx['vout']:
if outpoint['value'] == Decimal('2.20000000'):
vout = outpoint
break
vout = next(o for o in rawTx['vout'] if o['value'] == Decimal('2.20000000'))

bal = self.nodes[0].getbalance()
inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex']}]
Expand Down Expand Up @@ -279,11 +275,7 @@ def run_test(self):

txDetails = self.nodes[0].gettransaction(txId, True)
rawTx2 = self.nodes[0].decoderawtransaction(txDetails['hex'])
vout = False
for outpoint in rawTx2['vout']:
if outpoint['value'] == Decimal('2.20000000'):
vout = outpoint
break
vout = next(o for o in rawTx2['vout'] if o['value'] == Decimal('2.20000000'))

bal = self.nodes[0].getbalance()
inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "redeemScript" : mSigObjValid['hex']}]
Expand Down Expand Up @@ -375,5 +367,30 @@ def run_test(self):
decrawtx = self.nodes[0].decoderawtransaction(rawtx)
assert_equal(decrawtx['version'], 0x7fff)

self.log.info('sendrawtransaction/testmempoolaccept with maxfeerate')

txId = self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1.0)
rawTx = self.nodes[0].getrawtransaction(txId, True)
vout = next(o for o in rawTx['vout'] if o['value'] == Decimal('1.00000000'))

self.sync_all()
inputs = [{ "txid" : txId, "vout" : vout['n'] }]
outputs = { self.nodes[0].getnewaddress() : Decimal("0.99999000") } # 1000 sat fee
rawTx = self.nodes[2].createrawtransaction(inputs, outputs)
rawTxSigned = self.nodes[2].signrawtransactionwithwallet(rawTx)
assert_equal(rawTxSigned['complete'], True)
# 1000 sat fee, ~200 b transaction, fee rate should land around 5 sat/b = 0.00005000 BTC/kB
# Thus, testmempoolaccept should reject
testres = self.nodes[2].testmempoolaccept([rawTxSigned['hex']], 0.00001000)[0]
assert_equal(testres['allowed'], False)
assert_equal(testres['reject-reason'], '256: absurdly-high-fee')
# and sendrawtransaction should throw
assert_raises_rpc_error(-26, "absurdly-high-fee", self.nodes[2].sendrawtransaction, rawTxSigned['hex'], 0.00001000)
# And below calls should both succeed
testres = self.nodes[2].testmempoolaccept(rawtxs=[rawTxSigned['hex']], maxfeerate=0.00007000)[0]
assert_equal(testres['allowed'], True)
self.nodes[2].sendrawtransaction(hexstring=rawTxSigned['hex'], maxfeerate=0.00007000)


if __name__ == '__main__':
RawTransactionsTest().main()
4 changes: 2 additions & 2 deletions test/functional/test_framework/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants):

rawtx = from_node.createrawtransaction(inputs, outputs)
signresult = from_node.signrawtransactionwithwallet(rawtx)
txid = from_node.sendrawtransaction(signresult["hex"], True)
txid = from_node.sendrawtransaction(signresult["hex"], 0)

return (txid, signresult["hex"], fee)

Expand Down Expand Up @@ -642,7 +642,7 @@ def create_lots_of_big_transactions(node, txouts, utxos, num, fee):
tx.vout.append(txout)
newtx = tx.serialize().hex()
signresult = node.signrawtransactionwithwallet(newtx, None, "NONE")
txid = node.sendrawtransaction(signresult["hex"], True)
txid = node.sendrawtransaction(signresult["hex"], 0)
txids.append(txid)
return txids

Expand Down

0 comments on commit 99d82a8

Please sign in to comment.