Skip to content

Commit 63c63b5

Browse files
author
MarcoFalke
committed
Merge bitcoin/bitcoin#14707: [RPC] Include coinbase transactions in receivedby RPCs
1dcba99 Coinbase receivedby rpcs release notes (Andrew Toth) b569675 Test including coinbase transactions in receivedby wallet rpcs (Andrew Toth) bce20c3 Include coinbase transactions in receivedby wallet rpcs (Andrew Toth) Pull request description: The current `*receivedby*` RPCs filter out coinbase transactions. This doesn't seem correct since an output to your address in a coinbase transaction *is* receiving those coins. This PR corrects this behaviour. Also, a new option `include_immature_coinbase` is added (default=`false`) that includes immature coinbase transactions when set to true. However, since this is potentially a breaking change this PR introduces a hidden configuration option `-deprecatedrpc=exclude_coinbase`. This can be set to revert to previous behaviour. If no reports of broken workflow are received, then this option can be removed in a future release. Fixes bitcoin/bitcoin#14654. ACKs for top commit: jnewbery: reACK 1dcba99 Tree-SHA512: bfc43b81279fea5b6770a4620b196f6bc7c818d221b228623e9f535ec75a2406bc440e3df911608a3680f11ab64c5a4103917162114f5ff7c4ca8ab07bb9d3df
2 parents eaf1c56 + 1dcba99 commit 63c63b5

File tree

4 files changed

+197
-14
lines changed

4 files changed

+197
-14
lines changed

doc/release-notes-14707.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Wallet `receivedby` RPCs now include coinbase transactions
2+
-------------
3+
4+
Previously, the following wallet RPCs excluded coinbase transactions:
5+
6+
`getreceivedbyaddress`
7+
8+
`getreceivedbylabel`
9+
10+
`listreceivedbyaddress`
11+
12+
`listreceivedbylabel`
13+
14+
This release changes this behaviour and returns results accounting for received coins from coinbase outputs.
15+
16+
A new option, `include_immature_coinbase` (default=`false`), determines whether to account for immature coinbase transactions.
17+
Immature coinbase transactions are coinbase transactions that have 100 or fewer confirmations, and are not spendable.
18+
19+
The previous behaviour can be restored using the configuration `-deprecatedrpc=exclude_coinbase`, but may be removed in a future release.

src/rpc/client.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,17 @@ static const CRPCConvertParam vRPCConvertParams[] =
4646
{ "settxfee", 0, "amount" },
4747
{ "sethdseed", 0, "newkeypool" },
4848
{ "getreceivedbyaddress", 1, "minconf" },
49+
{ "getreceivedbyaddress", 2, "include_immature_coinbase" },
4950
{ "getreceivedbylabel", 1, "minconf" },
51+
{ "getreceivedbylabel", 2, "include_immature_coinbase" },
5052
{ "listreceivedbyaddress", 0, "minconf" },
5153
{ "listreceivedbyaddress", 1, "include_empty" },
5254
{ "listreceivedbyaddress", 2, "include_watchonly" },
55+
{ "listreceivedbyaddress", 4, "include_immature_coinbase" },
5356
{ "listreceivedbylabel", 0, "minconf" },
5457
{ "listreceivedbylabel", 1, "include_empty" },
5558
{ "listreceivedbylabel", 2, "include_watchonly" },
59+
{ "listreceivedbylabel", 3, "include_immature_coinbase" },
5660
{ "getbalance", 1, "minconf" },
5761
{ "getbalance", 2, "include_watchonly" },
5862
{ "getbalance", 3, "avoid_reuse" },

src/wallet/rpcwallet.cpp

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -528,11 +528,26 @@ static CAmount GetReceived(const CWallet& wallet, const UniValue& params, bool b
528528
if (!params[1].isNull())
529529
min_depth = params[1].get_int();
530530

531+
const bool include_immature_coinbase{params[2].isNull() ? false : params[2].get_bool()};
532+
533+
// Excluding coinbase outputs is deprecated
534+
// It can be enabled by setting deprecatedrpc=exclude_coinbase
535+
const bool include_coinbase{!wallet.chain().rpcEnableDeprecated("exclude_coinbase")};
536+
537+
if (include_immature_coinbase && !include_coinbase) {
538+
throw JSONRPCError(RPC_INVALID_PARAMETER, "include_immature_coinbase is incompatible with deprecated exclude_coinbase");
539+
}
540+
531541
// Tally
532542
CAmount amount = 0;
533543
for (const std::pair<const uint256, CWalletTx>& wtx_pair : wallet.mapWallet) {
534544
const CWalletTx& wtx = wtx_pair.second;
535-
if (wtx.IsCoinBase() || !wallet.chain().checkFinalTx(*wtx.tx) || wallet.GetTxDepthInMainChain(wtx) < min_depth) {
545+
int depth{wallet.GetTxDepthInMainChain(wtx)};
546+
if (depth < min_depth
547+
// Coinbase with less than 1 confirmation is no longer in the main chain
548+
|| (wtx.IsCoinBase() && (depth < 1 || !include_coinbase))
549+
|| (wallet.IsTxImmatureCoinBase(wtx) && !include_immature_coinbase)
550+
|| !wallet.chain().checkFinalTx(*wtx.tx)) {
536551
continue;
537552
}
538553

@@ -555,6 +570,7 @@ static RPCHelpMan getreceivedbyaddress()
555570
{
556571
{"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The bitcoin address for transactions."},
557572
{"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "Only include transactions confirmed at least this many times."},
573+
{"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."},
558574
},
559575
RPCResult{
560576
RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " received at this address."
@@ -566,6 +582,8 @@ static RPCHelpMan getreceivedbyaddress()
566582
+ HelpExampleCli("getreceivedbyaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 0") +
567583
"\nThe amount with at least 6 confirmations\n"
568584
+ HelpExampleCli("getreceivedbyaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 6") +
585+
"\nThe amount with at least 6 confirmations including immature coinbase outputs\n"
586+
+ HelpExampleCli("getreceivedbyaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 6 true") +
569587
"\nAs a JSON-RPC call\n"
570588
+ HelpExampleRpc("getreceivedbyaddress", "\"" + EXAMPLE_ADDRESS[0] + "\", 6")
571589
},
@@ -593,6 +611,7 @@ static RPCHelpMan getreceivedbylabel()
593611
{
594612
{"label", RPCArg::Type::STR, RPCArg::Optional::NO, "The selected label, may be the default label using \"\"."},
595613
{"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "Only include transactions confirmed at least this many times."},
614+
{"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."},
596615
},
597616
RPCResult{
598617
RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " received for this label."
@@ -604,8 +623,10 @@ static RPCHelpMan getreceivedbylabel()
604623
+ HelpExampleCli("getreceivedbylabel", "\"tabby\" 0") +
605624
"\nThe amount with at least 6 confirmations\n"
606625
+ HelpExampleCli("getreceivedbylabel", "\"tabby\" 6") +
626+
"\nThe amount with at least 6 confirmations including immature coinbase outputs\n"
627+
+ HelpExampleCli("getreceivedbylabel", "\"tabby\" 6 true") +
607628
"\nAs a JSON-RPC call\n"
608-
+ HelpExampleRpc("getreceivedbylabel", "\"tabby\", 6")
629+
+ HelpExampleRpc("getreceivedbylabel", "\"tabby\", 6, true")
609630
},
610631
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
611632
{
@@ -894,7 +915,7 @@ struct tallyitem
894915
}
895916
};
896917

897-
static UniValue ListReceived(const CWallet& wallet, const UniValue& params, bool by_label) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
918+
static UniValue ListReceived(const CWallet& wallet, const UniValue& params, const bool by_label, const bool include_immature_coinbase) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
898919
{
899920
// Minimum confirmations
900921
int nMinDepth = 1;
@@ -914,27 +935,38 @@ static UniValue ListReceived(const CWallet& wallet, const UniValue& params, bool
914935

915936
bool has_filtered_address = false;
916937
CTxDestination filtered_address = CNoDestination();
917-
if (!by_label && params.size() > 3) {
938+
if (!by_label && !params[3].isNull() && !params[3].get_str().empty()) {
918939
if (!IsValidDestinationString(params[3].get_str())) {
919940
throw JSONRPCError(RPC_WALLET_ERROR, "address_filter parameter was invalid");
920941
}
921942
filtered_address = DecodeDestination(params[3].get_str());
922943
has_filtered_address = true;
923944
}
924945

946+
// Excluding coinbase outputs is deprecated
947+
// It can be enabled by setting deprecatedrpc=exclude_coinbase
948+
const bool include_coinbase{!wallet.chain().rpcEnableDeprecated("exclude_coinbase")};
949+
950+
if (include_immature_coinbase && !include_coinbase) {
951+
throw JSONRPCError(RPC_INVALID_PARAMETER, "include_immature_coinbase is incompatible with deprecated exclude_coinbase");
952+
}
953+
925954
// Tally
926955
std::map<CTxDestination, tallyitem> mapTally;
927956
for (const std::pair<const uint256, CWalletTx>& pairWtx : wallet.mapWallet) {
928957
const CWalletTx& wtx = pairWtx.second;
929958

930-
if (wtx.IsCoinBase() || !wallet.chain().checkFinalTx(*wtx.tx)) {
931-
continue;
932-
}
933-
934959
int nDepth = wallet.GetTxDepthInMainChain(wtx);
935960
if (nDepth < nMinDepth)
936961
continue;
937962

963+
// Coinbase with less than 1 confirmation is no longer in the main chain
964+
if ((wtx.IsCoinBase() && (nDepth < 1 || !include_coinbase))
965+
|| (wallet.IsTxImmatureCoinBase(wtx) && !include_immature_coinbase)
966+
|| !wallet.chain().checkFinalTx(*wtx.tx)) {
967+
continue;
968+
}
969+
938970
for (const CTxOut& txout : wtx.tx->vout)
939971
{
940972
CTxDestination address;
@@ -1049,7 +1081,8 @@ static RPCHelpMan listreceivedbyaddress()
10491081
{"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "The minimum number of confirmations before payments are included."},
10501082
{"include_empty", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include addresses that haven't received any payments."},
10511083
{"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Whether to include watch-only addresses (see 'importaddress')"},
1052-
{"address_filter", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "If present, only return information on this address."},
1084+
{"address_filter", RPCArg::Type::STR, RPCArg::Optional::OMITTED_NAMED_ARG, "If present and non-empty, only return information on this address."},
1085+
{"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."},
10531086
},
10541087
RPCResult{
10551088
RPCResult::Type::ARR, "", "",
@@ -1071,8 +1104,9 @@ static RPCHelpMan listreceivedbyaddress()
10711104
RPCExamples{
10721105
HelpExampleCli("listreceivedbyaddress", "")
10731106
+ HelpExampleCli("listreceivedbyaddress", "6 true")
1107+
+ HelpExampleCli("listreceivedbyaddress", "6 true true \"\" true")
10741108
+ HelpExampleRpc("listreceivedbyaddress", "6, true, true")
1075-
+ HelpExampleRpc("listreceivedbyaddress", "6, true, true, \"" + EXAMPLE_ADDRESS[0] + "\"")
1109+
+ HelpExampleRpc("listreceivedbyaddress", "6, true, true, \"" + EXAMPLE_ADDRESS[0] + "\", true")
10761110
},
10771111
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
10781112
{
@@ -1083,9 +1117,11 @@ static RPCHelpMan listreceivedbyaddress()
10831117
// the user could have gotten from another RPC command prior to now
10841118
pwallet->BlockUntilSyncedToCurrentChain();
10851119

1120+
const bool include_immature_coinbase{request.params[4].isNull() ? false : request.params[4].get_bool()};
1121+
10861122
LOCK(pwallet->cs_wallet);
10871123

1088-
return ListReceived(*pwallet, request.params, false);
1124+
return ListReceived(*pwallet, request.params, false, include_immature_coinbase);
10891125
},
10901126
};
10911127
}
@@ -1098,6 +1134,7 @@ static RPCHelpMan listreceivedbylabel()
10981134
{"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "The minimum number of confirmations before payments are included."},
10991135
{"include_empty", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include labels that haven't received any payments."},
11001136
{"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Whether to include watch-only addresses (see 'importaddress')"},
1137+
{"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."},
11011138
},
11021139
RPCResult{
11031140
RPCResult::Type::ARR, "", "",
@@ -1114,7 +1151,7 @@ static RPCHelpMan listreceivedbylabel()
11141151
RPCExamples{
11151152
HelpExampleCli("listreceivedbylabel", "")
11161153
+ HelpExampleCli("listreceivedbylabel", "6 true")
1117-
+ HelpExampleRpc("listreceivedbylabel", "6, true, true")
1154+
+ HelpExampleRpc("listreceivedbylabel", "6, true, true, true")
11181155
},
11191156
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
11201157
{
@@ -1125,9 +1162,11 @@ static RPCHelpMan listreceivedbylabel()
11251162
// the user could have gotten from another RPC command prior to now
11261163
pwallet->BlockUntilSyncedToCurrentChain();
11271164

1165+
const bool include_immature_coinbase{request.params[3].isNull() ? false : request.params[3].get_bool()};
1166+
11281167
LOCK(pwallet->cs_wallet);
11291168

1130-
return ListReceived(*pwallet, request.params, true);
1169+
return ListReceived(*pwallet, request.params, true, include_immature_coinbase);
11311170
},
11321171
};
11331172
}

test/functional/wallet_listreceivedby.py

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
# Copyright (c) 2014-2021 The Bitcoin Core developers
33
# Distributed under the MIT software license, see the accompanying
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5-
"""Test the listreceivedbyaddress RPC."""
5+
"""Test the listreceivedbyaddress, listreceivedbylabel, getreceivedybaddress, and getreceivedbylabel RPCs."""
66
from decimal import Decimal
77

8+
from test_framework.blocktools import COINBASE_MATURITY
89
from test_framework.test_framework import BitcoinTestFramework
910
from test_framework.util import (
1011
assert_array_result,
@@ -17,6 +18,8 @@
1718
class ReceivedByTest(BitcoinTestFramework):
1819
def set_test_params(self):
1920
self.num_nodes = 2
21+
# Test deprecated exclude coinbase on second node
22+
self.extra_args = [[], ["-deprecatedrpc=exclude_coinbase"]]
2023

2124
def skip_test_if_missing_module(self):
2225
self.skip_if_no_wallet()
@@ -162,5 +165,123 @@ def run_test(self):
162165
balance = self.nodes[1].getreceivedbylabel("mynewlabel")
163166
assert_equal(balance, Decimal("0.0"))
164167

168+
self.log.info("Tests for including coinbase outputs")
169+
170+
# Generate block reward to address with label
171+
label = "label"
172+
address = self.nodes[0].getnewaddress(label)
173+
174+
reward = Decimal("25")
175+
self.generatetoaddress(self.nodes[0], 1, address, sync_fun=self.no_op)
176+
hash = self.nodes[0].getbestblockhash()
177+
178+
self.log.info("getreceivedbyaddress returns nothing with defaults")
179+
balance = self.nodes[0].getreceivedbyaddress(address)
180+
assert_equal(balance, 0)
181+
182+
self.log.info("getreceivedbyaddress returns block reward when including immature coinbase")
183+
balance = self.nodes[0].getreceivedbyaddress(address=address, include_immature_coinbase=True)
184+
assert_equal(balance, reward)
185+
186+
self.log.info("getreceivedbylabel returns nothing with defaults")
187+
balance = self.nodes[0].getreceivedbylabel("label")
188+
assert_equal(balance, 0)
189+
190+
self.log.info("getreceivedbylabel returns block reward when including immature coinbase")
191+
balance = self.nodes[0].getreceivedbylabel(label="label", include_immature_coinbase=True)
192+
assert_equal(balance, reward)
193+
194+
self.log.info("listreceivedbyaddress does not include address with defaults")
195+
assert_array_result(self.nodes[0].listreceivedbyaddress(),
196+
{"address": address},
197+
{}, True)
198+
199+
self.log.info("listreceivedbyaddress includes address when including immature coinbase")
200+
assert_array_result(self.nodes[0].listreceivedbyaddress(minconf=1, include_immature_coinbase=True),
201+
{"address": address},
202+
{"address": address, "amount": reward})
203+
204+
self.log.info("listreceivedbylabel does not include label with defaults")
205+
assert_array_result(self.nodes[0].listreceivedbylabel(),
206+
{"label": label},
207+
{}, True)
208+
209+
self.log.info("listreceivedbylabel includes label when including immature coinbase")
210+
assert_array_result(self.nodes[0].listreceivedbylabel(minconf=1, include_immature_coinbase=True),
211+
{"label": label},
212+
{"label": label, "amount": reward})
213+
214+
self.log.info("Generate 100 more blocks")
215+
self.generate(self.nodes[0], COINBASE_MATURITY, sync_fun=self.no_op)
216+
217+
self.log.info("getreceivedbyaddress returns reward with defaults")
218+
balance = self.nodes[0].getreceivedbyaddress(address)
219+
assert_equal(balance, reward)
220+
221+
self.log.info("getreceivedbylabel returns reward with defaults")
222+
balance = self.nodes[0].getreceivedbylabel("label")
223+
assert_equal(balance, reward)
224+
225+
self.log.info("listreceivedbyaddress includes address with defaults")
226+
assert_array_result(self.nodes[0].listreceivedbyaddress(),
227+
{"address": address},
228+
{"address": address, "amount": reward})
229+
230+
self.log.info("listreceivedbylabel includes label with defaults")
231+
assert_array_result(self.nodes[0].listreceivedbylabel(),
232+
{"label": label},
233+
{"label": label, "amount": reward})
234+
235+
self.log.info("Invalidate block that paid to address")
236+
self.nodes[0].invalidateblock(hash)
237+
238+
self.log.info("getreceivedbyaddress does not include invalidated block when minconf is 0 when including immature coinbase")
239+
balance = self.nodes[0].getreceivedbyaddress(address=address, minconf=0, include_immature_coinbase=True)
240+
assert_equal(balance, 0)
241+
242+
self.log.info("getreceivedbylabel does not include invalidated block when minconf is 0 when including immature coinbase")
243+
balance = self.nodes[0].getreceivedbylabel(label="label", minconf=0, include_immature_coinbase=True)
244+
assert_equal(balance, 0)
245+
246+
self.log.info("listreceivedbyaddress does not include invalidated block when minconf is 0 when including immature coinbase")
247+
assert_array_result(self.nodes[0].listreceivedbyaddress(minconf=0, include_immature_coinbase=True),
248+
{"address": address},
249+
{}, True)
250+
251+
self.log.info("listreceivedbylabel does not include invalidated block when minconf is 0 when including immature coinbase")
252+
assert_array_result(self.nodes[0].listreceivedbylabel(minconf=0, include_immature_coinbase=True),
253+
{"label": label},
254+
{}, True)
255+
256+
# Test exclude_coinbase
257+
address2 = self.nodes[1].getnewaddress(label)
258+
self.generatetoaddress(self.nodes[1], COINBASE_MATURITY + 1, address2, sync_fun=self.no_op)
259+
260+
self.log.info("getreceivedbyaddress returns nothing when excluding coinbase")
261+
balance = self.nodes[1].getreceivedbyaddress(address2)
262+
assert_equal(balance, 0)
263+
264+
self.log.info("getreceivedbylabel returns nothing when excluding coinbase")
265+
balance = self.nodes[1].getreceivedbylabel("label")
266+
assert_equal(balance, 0)
267+
268+
self.log.info("listreceivedbyaddress does not include address when excluding coinbase")
269+
assert_array_result(self.nodes[1].listreceivedbyaddress(),
270+
{"address": address2},
271+
{}, True)
272+
273+
self.log.info("listreceivedbylabel does not include label when excluding coinbase")
274+
assert_array_result(self.nodes[1].listreceivedbylabel(),
275+
{"label": label},
276+
{}, True)
277+
278+
self.log.info("getreceivedbyaddress throws when setting include_immature_coinbase with deprecated exclude_coinbase")
279+
assert_raises_rpc_error(-8, 'include_immature_coinbase is incompatible with deprecated exclude_coinbase', self.nodes[1].getreceivedbyaddress, address2, 1, True)
280+
281+
282+
self.log.info("listreceivedbyaddress throws when setting include_immature_coinbase with deprecated exclude_coinbase")
283+
assert_raises_rpc_error(-8, 'include_immature_coinbase is incompatible with deprecated exclude_coinbase', self.nodes[1].listreceivedbyaddress, 1, False, False, "", True)
284+
285+
165286
if __name__ == '__main__':
166287
ReceivedByTest().main()

0 commit comments

Comments
 (0)