Skip to content

Commit 2fb7b47

Browse files
author
alex v
authored
Accumulation of staking rewards in different address (#401)
* support for -stakingaddress argument * fix byteswap * adds support for multiaddress JSON mapping (#54) * fixes qt transaction display for -stakingaddress feature (#53) * fixes loop to only use the address from the last vout * avoid cfund vout in coinstake * Fixes list transaction rpc (#52) * fix list transaction rpc to show correct staking address * avoid cfund vout in coinstake and fix amount in GetReceived() * add test for -stakingaddress
1 parent 6025758 commit 2fb7b47

File tree

5 files changed

+232
-47
lines changed

5 files changed

+232
-47
lines changed

qa/pull-tester/rpc-tests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@
173173
'hardfork-451.py',
174174
'hardfork-452.py',
175175
'staticr-tx-send.py',
176+
'stakingaddress.py',
176177
'mnemonic.py',
177178
'sendtoaddress.py',
178179
'stakeimmaturebalance.py',

qa/rpc-tests/stakingaddress.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2019 The Navcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
from test_framework.test_framework import NavCoinTestFramework
7+
from test_framework.staticr_util import *
8+
9+
class StakingAddressTest(NavCoinTestFramework):
10+
"""Tests staking to another address using -stakingaddress option."""
11+
12+
def __init__(self):
13+
super().__init__()
14+
self.setup_clean_chain = True
15+
self.num_nodes = 1
16+
self.node_args = ["-stakingaddress="]
17+
18+
def setup_network(self, split=False):
19+
self.nodes = []
20+
self.nodes.append(start_node(0, self.options.tmpdir, []))
21+
self.is_network_split = split
22+
23+
def run_test(self):
24+
SATOSHI = 100000000
25+
COINBASE_REWARD = 5000000000
26+
27+
## Test stake to a single address ##
28+
# Get new address for receiving the staking rewards
29+
receive_addr_A = self.nodes[0].getnewaddress()
30+
31+
print("Restart node with staking address option ...")
32+
self.stop_node(0)
33+
self.nodes[0] = start_node(0, self.options.tmpdir, [self.node_args[0] + receive_addr_A])
34+
35+
# Activate static rewards
36+
activate_staticr(self.nodes[0])
37+
38+
# Wait for a stake
39+
stake_tx, reward_vout = self.get_stake_tx()
40+
41+
# check that the staking reward was sent to the receiving address
42+
assert_equal(self.nodes[0].gettransaction(stake_tx)["details"][0]["amount"], 2)
43+
assert_equal(self.nodes[0].gettransaction(stake_tx)["details"][0]["address"], receive_addr_A)
44+
assert_equal(reward_vout["scriptPubKey"]["addresses"][0], receive_addr_A)
45+
46+
47+
## Test stake address mapping ##
48+
# Get new addresses to hold staking coins and for receiving the staking rewards
49+
staking_addr_B = self.nodes[0].getnewaddress()
50+
receive_addr_B = self.nodes[0].getnewaddress()
51+
52+
print("Restart node with staking address option ...")
53+
self.stop_node(0)
54+
staking_map = '{"' + staking_addr_B + '":"' + receive_addr_B + '"}'
55+
self.nodes[0] = start_node(0, self.options.tmpdir, [self.node_args[0] + staking_map])
56+
57+
# Stop staking, make all coins mature and send all to one address
58+
self.nodes[0].staking(False)
59+
slow_gen(self.nodes[0], 50)
60+
self.nodes[0].sendtoaddress(staking_addr_B, self.nodes[0].getbalance(), "", "", "", True)
61+
time.sleep(2)
62+
slow_gen(self.nodes[0], 1)
63+
fee = 1000000
64+
total_fee = 0
65+
for unspent in self.nodes[0].listunspent():
66+
if unspent["amount"] == COINBASE_REWARD / SATOSHI:
67+
# Send away any additional coinbase reward txs which might be staked instead
68+
tx = self.nodes[0].createrawtransaction([unspent], {self.nodes[0].getnewaddress(): float(COINBASE_REWARD - fee) / SATOSHI})
69+
txid = self.nodes[0].sendrawtransaction(self.nodes[0].signrawtransaction(tx)["hex"])
70+
total_fee += fee
71+
72+
assert(len(self.nodes[0].listunspent()) == 1)
73+
assert(self.nodes[0].listunspent()[0]["amount"] > COINBASE_REWARD / SATOSHI)
74+
75+
# Wait for a new block to be staked from the large utxo sent to staking_addr_B
76+
self.nodes[0].staking(True)
77+
stake_tx, reward_vout = self.get_stake_tx(total_fee)
78+
79+
# check that the staking reward was sent to the receiving address
80+
assert_equal(float(self.nodes[0].gettransaction(stake_tx)["details"][0]["amount"]), (2.0 * SATOSHI + total_fee) / SATOSHI)
81+
assert_equal(self.nodes[0].gettransaction(stake_tx)["details"][0]["address"], receive_addr_B)
82+
assert_equal(reward_vout["scriptPubKey"]["addresses"][0], receive_addr_B)
83+
84+
85+
## Test stake address mapping using 'all' option ##
86+
# Get new address for receiving the staking rewards
87+
receive_addr_C = self.nodes[0].getnewaddress()
88+
89+
print("Restart node with staking address option ...")
90+
self.stop_node(0)
91+
staking_map = '{"all":"' + receive_addr_C + '"}'
92+
self.nodes[0] = start_node(0, self.options.tmpdir, [self.node_args[0] + staking_map])
93+
94+
# Wait for a stake
95+
stake_tx, reward_vout = self.get_stake_tx()
96+
97+
# check that the staking reward was sent to the receiving address
98+
assert_equal(self.nodes[0].gettransaction(stake_tx)["details"][0]["amount"], 2)
99+
assert_equal(self.nodes[0].gettransaction(stake_tx)["details"][0]["address"], receive_addr_C)
100+
assert_equal(reward_vout["scriptPubKey"]["addresses"][0], receive_addr_C)
101+
102+
103+
def get_stake_tx(self, tx_fee=0):
104+
SATOSHI = 100000000
105+
blockcount = self.nodes[0].getblockcount()
106+
107+
# wait for a new block to be staked
108+
while self.nodes[0].getblockcount() == blockcount:
109+
print("waiting for a new block...")
110+
time.sleep(5)
111+
112+
stake_tx = self.nodes[0].getblock(self.nodes[0].getbestblockhash())["tx"][1]
113+
stake_vouts = self.nodes[0].decoderawtransaction(self.nodes[0].gettransaction(stake_tx)["hex"])["vout"]
114+
i = -1
115+
reward_vout = stake_vouts[i]
116+
while float(stake_vouts[i]["value"]) != (2.0 * SATOSHI + tx_fee) / SATOSHI:
117+
# The staking reward can also contain a cfund contribution which must be skipped
118+
i -= 1
119+
reward_vout = stake_vouts[i]
120+
121+
return stake_tx, reward_vout
122+
123+
if __name__ == '__main__':
124+
StakingAddressTest().main()

src/qt/transactionrecord.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ QList<TransactionRecord> TransactionRecord::decomposeTransaction(const CWallet *
4040
CAmount nDebit = wtx.GetDebit((wtx.IsCoinStake() && wtx.vout[1].scriptPubKey.IsColdStaking()) ? ISMINE_STAKABLE : ISMINE_ALL);
4141
CAmount nCFundCredit = wtx.GetDebit(ISMINE_ALL);
4242
CAmount nNet = nCredit - nDebit;
43-
uint256 hash = wtx.GetHash(), hashPrev = uint256();
43+
uint256 hash = wtx.GetHash();
4444
std::map<std::string, std::string> mapValue = wtx.mapValue;
4545
std::string dzeel = "";
4646

@@ -55,6 +55,11 @@ QList<TransactionRecord> TransactionRecord::decomposeTransaction(const CWallet *
5555
// Credit
5656
//
5757
unsigned int i = 0;
58+
unsigned int rewardIdx = 0;
59+
if (wtx.IsCoinStake())
60+
// If coinstake has no cfund contribution, get details of last vout or else use second to last
61+
rewardIdx = wtx.vout.size() - (wtx.GetValueOutCFund() == 0 ? 1 : 2);
62+
5863
BOOST_FOREACH(const CTxOut& txout, wtx.vout)
5964
{
6065
isminetype mine = wallet->IsMine(txout);
@@ -88,12 +93,14 @@ QList<TransactionRecord> TransactionRecord::decomposeTransaction(const CWallet *
8893
{
8994
// Generated (proof-of-stake)
9095

91-
if (hashPrev == hash)
92-
continue; // last coinstake output
96+
if (i != rewardIdx)
97+
{
98+
i++;
99+
continue; // only append details of the address with reward output
100+
}
93101

94102
sub.type = TransactionRecord::Staked;
95103
sub.credit = nNet > 0 ? nNet : wtx.GetValueOut() - nDebit - wtx.GetValueOutCFund();
96-
hashPrev = hash;
97104
}
98105
if(wtx.fAnon)
99106
{

src/wallet/rpcwallet.cpp

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1830,6 +1830,49 @@ static void MaybePushAddress(UniValue & entry, const CTxDestination &dest)
18301830
}
18311831
}
18321832

1833+
void GetReceived(const COutputEntry& r, const CWalletTx& wtx, const string& strAccount, bool fLong, UniValue& ret, CAmount nFee, bool fAllAccounts, bool involvesWatchonly)
1834+
{
1835+
string account;
1836+
if (pwalletMain->mapAddressBook.count(r.destination))
1837+
account = pwalletMain->mapAddressBook[r.destination].name;
1838+
if (fAllAccounts || (account == strAccount))
1839+
{
1840+
UniValue entry(UniValue::VOBJ);
1841+
if(involvesWatchonly || (::IsMine(*pwalletMain, r.destination) & ISMINE_WATCH_ONLY))
1842+
entry.push_back(Pair("involvesWatchonly", true));
1843+
entry.push_back(Pair("account", account));
1844+
MaybePushAddress(entry, r.destination);
1845+
if (wtx.IsCoinBase() || wtx.IsCoinStake())
1846+
{
1847+
if (wtx.GetDepthInMainChain() < 1)
1848+
entry.push_back(Pair("category", "orphan"));
1849+
else if (wtx.GetBlocksToMaturity() > 0)
1850+
entry.push_back(Pair("category", "immature"));
1851+
else
1852+
entry.push_back(Pair("category", "generate"));
1853+
}
1854+
else
1855+
{
1856+
entry.push_back(Pair("category", "receive"));
1857+
}
1858+
if (!wtx.IsCoinStake())
1859+
entry.push_back(Pair("amount", ValueFromAmount(r.amount)));
1860+
else {
1861+
entry.push_back(Pair("amount", ValueFromAmount(-nFee)));
1862+
}
1863+
entry.push_back(Pair("canStake", (::IsMine(*pwalletMain, r.destination) & ISMINE_STAKABLE ||
1864+
(::IsMine(*pwalletMain, r.destination) & ISMINE_SPENDABLE &&
1865+
!CNavCoinAddress(r.destination).IsColdStakingAddress(Params()))) ? true : false));
1866+
entry.push_back(Pair("canSpend", (::IsMine(*pwalletMain, r.destination) & ISMINE_SPENDABLE) ? true : false));
1867+
if (pwalletMain->mapAddressBook.count(r.destination))
1868+
entry.push_back(Pair("label", account));
1869+
entry.push_back(Pair("vout", r.vout));
1870+
if (fLong)
1871+
WalletTxToJSON(wtx, entry);
1872+
ret.push_back(entry);
1873+
}
1874+
}
1875+
18331876
void ListTransactions(const CWalletTx& wtx, const string& strAccount, int nMinDepth, bool fLong, UniValue& ret, const isminefilter& filter)
18341877
{
18351878
CAmount nFee;
@@ -1874,53 +1917,21 @@ void ListTransactions(const CWalletTx& wtx, const string& strAccount, int nMinDe
18741917
// Received
18751918
if (listReceived.size() > 0 && wtx.GetDepthInMainChain() >= nMinDepth)
18761919
{
1877-
bool stop = false;
1878-
BOOST_FOREACH(const COutputEntry& r, listReceived)
1920+
if (!wtx.IsCoinStake())
18791921
{
1880-
string account;
1881-
if (pwalletMain->mapAddressBook.count(r.destination))
1882-
account = pwalletMain->mapAddressBook[r.destination].name;
1883-
if (fAllAccounts || (account == strAccount))
1922+
BOOST_FOREACH(const COutputEntry& r, listReceived)
18841923
{
1885-
UniValue entry(UniValue::VOBJ);
1886-
if(involvesWatchonly || (::IsMine(*pwalletMain, r.destination) & ISMINE_WATCH_ONLY))
1887-
entry.push_back(Pair("involvesWatchonly", true));
1888-
entry.push_back(Pair("account", account));
1889-
MaybePushAddress(entry, r.destination);
1890-
if (wtx.IsCoinBase() || wtx.IsCoinStake())
1891-
{
1892-
if (wtx.GetDepthInMainChain() < 1)
1893-
entry.push_back(Pair("category", "orphan"));
1894-
else if (wtx.GetBlocksToMaturity() > 0)
1895-
entry.push_back(Pair("category", "immature"));
1896-
else
1897-
entry.push_back(Pair("category", "generate"));
1898-
}
1899-
else
1900-
{
1901-
entry.push_back(Pair("category", "receive"));
1902-
}
1903-
if (!wtx.IsCoinStake())
1904-
entry.push_back(Pair("amount", ValueFromAmount(r.amount)));
1905-
else
1906-
{
1907-
entry.push_back(Pair("amount", ValueFromAmount(-nFee)));
1908-
stop = true; // only one coinstake output
1909-
}
1910-
entry.push_back(Pair("canStake", (::IsMine(*pwalletMain, r.destination) & ISMINE_STAKABLE ||
1911-
(::IsMine(*pwalletMain, r.destination) & ISMINE_SPENDABLE &&
1912-
!CNavCoinAddress(r.destination).IsColdStakingAddress(Params()))) ? true : false));
1913-
entry.push_back(Pair("canSpend", (::IsMine(*pwalletMain, r.destination) & ISMINE_SPENDABLE) ? true : false));
1914-
if (pwalletMain->mapAddressBook.count(r.destination))
1915-
entry.push_back(Pair("label", account));
1916-
entry.push_back(Pair("vout", r.vout));
1917-
if (fLong)
1918-
WalletTxToJSON(wtx, entry);
1919-
ret.push_back(entry);
1920-
if (stop)
1921-
break;
1924+
GetReceived(r, wtx, strAccount, fLong, ret, nFee, fAllAccounts, involvesWatchonly);
19221925
}
19231926
}
1927+
else
1928+
{
1929+
// only get the coinstake reward output
1930+
if (wtx.GetValueOutCFund() == 0)
1931+
GetReceived(listReceived.back(), wtx, strAccount, fLong, ret, nFee, fAllAccounts, involvesWatchonly);
1932+
else
1933+
GetReceived(*std::prev(listReceived.end(),1), wtx, strAccount, fLong, ret, nFee, fAllAccounts, involvesWatchonly);
1934+
}
19241935
}
19251936
}
19261937

src/wallet/wallet.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,47 @@ bool CWallet::CreateCoinStake(const CKeyStore& keystore, unsigned int nBits, int
546546
txNew.vout[1].nValue = blockValue;
547547
}
548548

549+
if (GetArg("-stakingaddress", "") != "" && !txNew.vout[txNew.vout.size()-1].scriptPubKey.IsColdStaking()) {
550+
CNavCoinAddress address;
551+
UniValue stakingAddress;
552+
UniValue addressMap(UniValue::VOBJ);
553+
554+
if (stakingAddress.read(GetArg("-stakingaddress", "")))
555+
{
556+
try {
557+
if (stakingAddress.isObject())
558+
addressMap = stakingAddress.get_obj();
559+
else
560+
return error("%s: Failed to read JSON from -stakingaddress argument", __func__);
561+
562+
// Use "all" address if present
563+
if(find_value(addressMap, "all").isStr())
564+
{
565+
address = CNavCoinAddress(find_value(addressMap, "all").get_str());
566+
}
567+
// Or use specified address if present
568+
if(find_value(addressMap, CNavCoinAddress(key.GetPubKey().GetID()).ToString()).isStr())
569+
{
570+
address = CNavCoinAddress(find_value(addressMap, CNavCoinAddress(key.GetPubKey().GetID()).ToString()).get_str());
571+
}
572+
573+
} catch (const UniValue& objError) {
574+
return error("%s: Failed to read JSON from -stakingaddress argument", __func__);
575+
} catch (const std::exception& e) {
576+
return error("%s: Failed to read JSON from -stakingaddress argument", __func__);
577+
}
578+
}
579+
else
580+
{
581+
address = CNavCoinAddress(GetArg("-stakingaddress", ""));
582+
}
583+
584+
if (address.IsValid()) {
585+
txNew.vout[txNew.vout.size()-1].nValue -= nReward;
586+
txNew.vout.push_back(CTxOut(nReward, GetScriptForDestination(address.Get())));
587+
}
588+
}
589+
549590
// Adds Community Fund output if enabled
550591
if(IsCommunityFundAccumulationEnabled(pindexPrev, Params().GetConsensus(), false))
551592
{
@@ -3893,6 +3934,7 @@ std::string CWallet::GetWalletHelpString(bool showDebug)
38933934
if (showDebug)
38943935
strUsage += HelpMessageOpt("-sendfreetransactions", strprintf(_("Send transactions as zero-fee transactions if possible (default: %u)"), DEFAULT_SEND_FREE_TRANSACTIONS));
38953936
strUsage += HelpMessageOpt("-spendzeroconfchange", strprintf(_("Spend unconfirmed change when sending transactions (default: %u)"), DEFAULT_SPEND_ZEROCONF_CHANGE));
3937+
strUsage += HelpMessageOpt("-stakingaddress", strprintf(_("Specify a customised navcoin address to accumulate the staking rewards.")));
38963938
strUsage += HelpMessageOpt("-txconfirmtarget=<n>", strprintf(_("If paytxfee is not set, include enough fee so transactions begin confirmation on average within n blocks (default: %u)"), DEFAULT_TX_CONFIRM_TARGET));
38973939
strUsage += HelpMessageOpt("-usehd", _("Use hierarchical deterministic key generation (HD) after BIP32. Only has effect during wallet creation/first start") + " " + strprintf(_("(default: %u)"), DEFAULT_USE_HD_WALLET));
38983940
strUsage += HelpMessageOpt("-upgradewallet", _("Upgrade wallet to latest format on startup"));

0 commit comments

Comments
 (0)