Skip to content

Commit 453bda6

Browse files
committed
Add 'subtractFeeFromOutputs' option to 'fundrawtransaction'.
1 parent 26fe5c9 commit 453bda6

File tree

5 files changed

+117
-14
lines changed

5 files changed

+117
-14
lines changed

qa/rpc-tests/fundrawtransaction.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,5 +660,75 @@ def run_test(self):
660660
assert_fee_amount(result2['fee'], count_bytes(result2['hex']), 2 * result_fee_rate)
661661
assert_fee_amount(result3['fee'], count_bytes(result3['hex']), 10 * result_fee_rate)
662662

663+
######################################
664+
# Test subtractFeeFromOutputs option #
665+
######################################
666+
667+
# Make sure there is exactly one input so coin selection can't skew the result
668+
assert_equal(len(self.nodes[3].listunspent(1)), 1)
669+
670+
inputs = []
671+
outputs = {self.nodes[2].getnewaddress(): 1}
672+
rawtx = self.nodes[3].createrawtransaction(inputs, outputs)
673+
674+
result = [self.nodes[3].fundrawtransaction(rawtx), # uses min_relay_tx_fee (set by settxfee)
675+
self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": []}), # empty subtraction list
676+
self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": [0]}), # uses min_relay_tx_fee (set by settxfee)
677+
self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 2*min_relay_tx_fee}),
678+
self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 2*min_relay_tx_fee, "subtractFeeFromOutputs": [0]})]
679+
680+
dec_tx = [self.nodes[3].decoderawtransaction(tx['hex']) for tx in result]
681+
output = [d['vout'][1 - r['changepos']]['value'] for d, r in zip(dec_tx, result)]
682+
change = [d['vout'][r['changepos']]['value'] for d, r in zip(dec_tx, result)]
683+
684+
assert_equal(result[0]['fee'], result[1]['fee'], result[2]['fee'])
685+
assert_equal(result[3]['fee'], result[4]['fee'])
686+
assert_equal(change[0], change[1])
687+
assert_equal(output[0], output[1])
688+
assert_equal(output[0], output[2] + result[2]['fee'])
689+
assert_equal(change[0] + result[0]['fee'], change[2])
690+
assert_equal(output[3], output[4] + result[4]['fee'])
691+
assert_equal(change[3] + result[3]['fee'], change[4])
692+
693+
inputs = []
694+
outputs = {self.nodes[2].getnewaddress(): value for value in (1.0, 1.1, 1.2, 1.3)}
695+
keys = list(outputs.keys())
696+
rawtx = self.nodes[3].createrawtransaction(inputs, outputs)
697+
698+
result = [self.nodes[3].fundrawtransaction(rawtx),
699+
# split the fee between outputs 0, 2, and 3, but not output 1
700+
self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": [0, 2, 3]})]
701+
702+
dec_tx = [self.nodes[3].decoderawtransaction(result[0]['hex']),
703+
self.nodes[3].decoderawtransaction(result[1]['hex'])]
704+
705+
# Nested list of non-change output amounts for each transaction
706+
output = [[out['value'] for i, out in enumerate(d['vout']) if i != r['changepos']]
707+
for d, r in zip(dec_tx, result)]
708+
709+
# List of differences in output amounts between normal and subtractFee transactions
710+
share = [o0 - o1 for o0, o1 in zip(output[0], output[1])]
711+
712+
# output 1 is the same in both transactions
713+
assert_equal(share[1], 0)
714+
715+
# the other 3 outputs are smaller as a result of subtractFeeFromOutputs
716+
assert_greater_than(share[0], 0)
717+
assert_greater_than(share[2], 0)
718+
assert_greater_than(share[3], 0)
719+
720+
# outputs 2 and 3 take the same share of the fee
721+
assert_equal(share[2], share[3])
722+
723+
# output 0 takes at least as much share of the fee, and no more than 2 satoshis more, than outputs 2 and 3
724+
assert_greater_than_or_equal(share[0], share[2])
725+
assert_greater_than_or_equal(share[2] + Decimal(2e-8), share[0])
726+
727+
# the fee is the same in both transactions
728+
assert_equal(result[0]['fee'], result[1]['fee'])
729+
730+
# the total subtracted from the outputs is equal to the fee
731+
assert_equal(share[0] + share[2] + share[3], result[0]['fee'])
732+
663733
if __name__ == '__main__':
664734
RawTransactionsTest().main()

qa/rpc-tests/test_framework/util.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -524,14 +524,18 @@ def assert_fee_amount(fee, tx_size, fee_per_kB):
524524
if fee > (tx_size + 2) * fee_per_kB / 1000:
525525
raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)"%(str(fee), str(target_fee)))
526526

527-
def assert_equal(thing1, thing2):
528-
if thing1 != thing2:
529-
raise AssertionError("%s != %s"%(str(thing1),str(thing2)))
527+
def assert_equal(thing1, thing2, *args):
528+
if thing1 != thing2 or any(thing1 != arg for arg in args):
529+
raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
530530

531531
def assert_greater_than(thing1, thing2):
532532
if thing1 <= thing2:
533533
raise AssertionError("%s <= %s"%(str(thing1),str(thing2)))
534534

535+
def assert_greater_than_or_equal(thing1, thing2):
536+
if thing1 < thing2:
537+
raise AssertionError("%s < %s"%(str(thing1),str(thing2)))
538+
535539
def assert_raises(exc, fun, *args, **kwds):
536540
assert_raises_message(exc, None, fun, *args, **kwds)
537541

src/wallet/rpcwallet.cpp

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2464,7 +2464,8 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
24642464
throw runtime_error(
24652465
"fundrawtransaction \"hexstring\" ( options )\n"
24662466
"\nAdd inputs to a transaction until it has enough in value to meet its out value.\n"
2467-
"This will not modify existing inputs, and will add one change output to the outputs.\n"
2467+
"This will not modify existing inputs, and will add at most one change output to the outputs.\n"
2468+
"No existing outputs will be modified unless \"subtractFeeFromOutputs\" is specified.\n"
24682469
"Note that inputs which were signed may need to be resigned after completion since in/outputs have been added.\n"
24692470
"The inputs added will not be signed, use signrawtransaction for that.\n"
24702471
"Note that all existing inputs must have their previous output transaction be in the wallet.\n"
@@ -2476,11 +2477,17 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
24762477
"1. \"hexstring\" (string, required) The hex string of the raw transaction\n"
24772478
"2. options (object, optional)\n"
24782479
" {\n"
2479-
" \"changeAddress\" (string, optional, default pool address) The bitcoin address to receive the change\n"
2480-
" \"changePosition\" (numeric, optional, default random) The index of the change output\n"
2481-
" \"includeWatching\" (boolean, optional, default false) Also select inputs which are watch only\n"
2482-
" \"lockUnspents\" (boolean, optional, default false) Lock selected unspent outputs\n"
2483-
" \"feeRate\" (numeric, optional, default not set: makes wallet determine the fee) Set a specific feerate (" + CURRENCY_UNIT + " per KB)\n"
2480+
" \"changeAddress\" (string, optional, default pool address) The bitcoin address to receive the change\n"
2481+
" \"changePosition\" (numeric, optional, default random) The index of the change output\n"
2482+
" \"includeWatching\" (boolean, optional, default false) Also select inputs which are watch only\n"
2483+
" \"lockUnspents\" (boolean, optional, default false) Lock selected unspent outputs\n"
2484+
" \"feeRate\" (numeric, optional, default not set: makes wallet determine the fee) Set a specific feerate (" + CURRENCY_UNIT + " per KB)\n"
2485+
" \"subtractFeeFromOutputs\" (array, optional) A json array of integers.\n"
2486+
" The fee will be equally deducted from the amount of each specified output.\n"
2487+
" The outputs are specified by their zero-based index, before any change output is added.\n"
2488+
" Those recipients will receive less bitcoins than you enter in their corresponding amount field.\n"
2489+
" If no outputs are specified here, the sender pays the fee.\n"
2490+
" [vout_index,...]\n"
24842491
" }\n"
24852492
" for backward compatibility: passing in a true instead of an object will result in {\"includeWatching\":true}\n"
24862493
"\nResult:\n"
@@ -2509,6 +2516,8 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
25092516
bool lockUnspents = false;
25102517
CFeeRate feeRate = CFeeRate(0);
25112518
bool overrideEstimatedFeerate = false;
2519+
UniValue subtractFeeFromOutputs;
2520+
set<int> setSubtractFeeFromOutputs;
25122521

25132522
if (request.params.size() > 1) {
25142523
if (request.params[1].type() == UniValue::VBOOL) {
@@ -2527,6 +2536,7 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
25272536
{"includeWatching", UniValueType(UniValue::VBOOL)},
25282537
{"lockUnspents", UniValueType(UniValue::VBOOL)},
25292538
{"feeRate", UniValueType()}, // will be checked below
2539+
{"subtractFeeFromOutputs", UniValueType(UniValue::VARR)},
25302540
},
25312541
true, true);
25322542

@@ -2553,6 +2563,9 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
25532563
feeRate = CFeeRate(AmountFromValue(options["feeRate"]));
25542564
overrideEstimatedFeerate = true;
25552565
}
2566+
2567+
if (options.exists("subtractFeeFromOutputs"))
2568+
subtractFeeFromOutputs = options["subtractFeeFromOutputs"].get_array();
25562569
}
25572570
}
25582571

@@ -2567,10 +2580,21 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
25672580
if (changePosition != -1 && (changePosition < 0 || (unsigned int)changePosition > tx.vout.size()))
25682581
throw JSONRPCError(RPC_INVALID_PARAMETER, "changePosition out of bounds");
25692582

2583+
for (unsigned int idx = 0; idx < subtractFeeFromOutputs.size(); idx++) {
2584+
int pos = subtractFeeFromOutputs[idx].get_int();
2585+
if (setSubtractFeeFromOutputs.count(pos))
2586+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, duplicated position: %d", pos));
2587+
if (pos < 0)
2588+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, negative position: %d", pos));
2589+
if (pos >= int(tx.vout.size()))
2590+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, position too large: %d", pos));
2591+
setSubtractFeeFromOutputs.insert(pos);
2592+
}
2593+
25702594
CAmount nFeeOut;
25712595
string strFailReason;
25722596

2573-
if(!pwalletMain->FundTransaction(tx, nFeeOut, overrideEstimatedFeerate, feeRate, changePosition, strFailReason, includeWatching, lockUnspents, changeAddress))
2597+
if(!pwalletMain->FundTransaction(tx, nFeeOut, overrideEstimatedFeerate, feeRate, changePosition, strFailReason, includeWatching, lockUnspents, setSubtractFeeFromOutputs, changeAddress))
25742598
throw JSONRPCError(RPC_INTERNAL_ERROR, strFailReason);
25752599

25762600
UniValue result(UniValue::VOBJ);

src/wallet/wallet.cpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2181,14 +2181,15 @@ bool CWallet::SelectCoins(const vector<COutput>& vAvailableCoins, const CAmount&
21812181
return res;
21822182
}
21832183

2184-
bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool overrideEstimatedFeeRate, const CFeeRate& specificFeeRate, int& nChangePosInOut, std::string& strFailReason, bool includeWatching, bool lockUnspents, const CTxDestination& destChange)
2184+
bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool overrideEstimatedFeeRate, const CFeeRate& specificFeeRate, int& nChangePosInOut, std::string& strFailReason, bool includeWatching, bool lockUnspents, const std::set<int>& setSubtractFeeFromOutputs, const CTxDestination& destChange)
21852185
{
21862186
vector<CRecipient> vecSend;
21872187

21882188
// Turn the txout set into a CRecipient vector
2189-
BOOST_FOREACH(const CTxOut& txOut, tx.vout)
2189+
for (size_t idx = 0; idx < tx.vout.size(); idx++)
21902190
{
2191-
CRecipient recipient = {txOut.scriptPubKey, txOut.nValue, false};
2191+
const CTxOut& txOut = tx.vout[idx];
2192+
CRecipient recipient = {txOut.scriptPubKey, txOut.nValue, setSubtractFeeFromOutputs.count(idx) == 1};
21922193
vecSend.push_back(recipient);
21932194
}
21942195

@@ -2210,6 +2211,10 @@ bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool ov
22102211
if (nChangePosInOut != -1)
22112212
tx.vout.insert(tx.vout.begin() + nChangePosInOut, wtx.tx->vout[nChangePosInOut]);
22122213

2214+
// Copy output sizes from new transaction; they may have had the fee subtracted from them
2215+
for (unsigned int idx = 0; idx < tx.vout.size(); idx++)
2216+
tx.vout[idx].nValue = wtx.tx->vout[idx].nValue;
2217+
22132218
// Add new txins (keeping original txin scriptSig/order)
22142219
BOOST_FOREACH(const CTxIn& txin, wtx.tx->vin)
22152220
{

src/wallet/wallet.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface
772772
* Insert additional inputs into the transaction by
773773
* calling CreateTransaction();
774774
*/
775-
bool FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool overrideEstimatedFeeRate, const CFeeRate& specificFeeRate, int& nChangePosInOut, std::string& strFailReason, bool includeWatching, bool lockUnspents, const CTxDestination& destChange = CNoDestination());
775+
bool FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool overrideEstimatedFeeRate, const CFeeRate& specificFeeRate, int& nChangePosInOut, std::string& strFailReason, bool includeWatching, bool lockUnspents, const std::set<int>& setSubtractFeeFromOutputs, const CTxDestination& destChange = CNoDestination());
776776

777777
/**
778778
* Create a new transaction paying the recipients with a set of coins

0 commit comments

Comments
 (0)