Skip to content

Commit

Permalink
Accumulation of staking rewards in different address (#401)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
alex v committed Mar 28, 2019
1 parent 6025758 commit 2fb7b47
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 47 deletions.
1 change: 1 addition & 0 deletions qa/pull-tester/rpc-tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
'hardfork-451.py',
'hardfork-452.py',
'staticr-tx-send.py',
'stakingaddress.py',
'mnemonic.py',
'sendtoaddress.py',
'stakeimmaturebalance.py',
Expand Down
124 changes: 124 additions & 0 deletions qa/rpc-tests/stakingaddress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
# Copyright (c) 2019 The Navcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.

from test_framework.test_framework import NavCoinTestFramework
from test_framework.staticr_util import *

class StakingAddressTest(NavCoinTestFramework):
"""Tests staking to another address using -stakingaddress option."""

def __init__(self):
super().__init__()
self.setup_clean_chain = True
self.num_nodes = 1
self.node_args = ["-stakingaddress="]

def setup_network(self, split=False):
self.nodes = []
self.nodes.append(start_node(0, self.options.tmpdir, []))
self.is_network_split = split

def run_test(self):
SATOSHI = 100000000
COINBASE_REWARD = 5000000000

## Test stake to a single address ##
# Get new address for receiving the staking rewards
receive_addr_A = self.nodes[0].getnewaddress()

print("Restart node with staking address option ...")
self.stop_node(0)
self.nodes[0] = start_node(0, self.options.tmpdir, [self.node_args[0] + receive_addr_A])

# Activate static rewards
activate_staticr(self.nodes[0])

# Wait for a stake
stake_tx, reward_vout = self.get_stake_tx()

# check that the staking reward was sent to the receiving address
assert_equal(self.nodes[0].gettransaction(stake_tx)["details"][0]["amount"], 2)
assert_equal(self.nodes[0].gettransaction(stake_tx)["details"][0]["address"], receive_addr_A)
assert_equal(reward_vout["scriptPubKey"]["addresses"][0], receive_addr_A)


## Test stake address mapping ##
# Get new addresses to hold staking coins and for receiving the staking rewards
staking_addr_B = self.nodes[0].getnewaddress()
receive_addr_B = self.nodes[0].getnewaddress()

print("Restart node with staking address option ...")
self.stop_node(0)
staking_map = '{"' + staking_addr_B + '":"' + receive_addr_B + '"}'
self.nodes[0] = start_node(0, self.options.tmpdir, [self.node_args[0] + staking_map])

# Stop staking, make all coins mature and send all to one address
self.nodes[0].staking(False)
slow_gen(self.nodes[0], 50)
self.nodes[0].sendtoaddress(staking_addr_B, self.nodes[0].getbalance(), "", "", "", True)
time.sleep(2)
slow_gen(self.nodes[0], 1)
fee = 1000000
total_fee = 0
for unspent in self.nodes[0].listunspent():
if unspent["amount"] == COINBASE_REWARD / SATOSHI:
# Send away any additional coinbase reward txs which might be staked instead
tx = self.nodes[0].createrawtransaction([unspent], {self.nodes[0].getnewaddress(): float(COINBASE_REWARD - fee) / SATOSHI})
txid = self.nodes[0].sendrawtransaction(self.nodes[0].signrawtransaction(tx)["hex"])
total_fee += fee

assert(len(self.nodes[0].listunspent()) == 1)
assert(self.nodes[0].listunspent()[0]["amount"] > COINBASE_REWARD / SATOSHI)

# Wait for a new block to be staked from the large utxo sent to staking_addr_B
self.nodes[0].staking(True)
stake_tx, reward_vout = self.get_stake_tx(total_fee)

# check that the staking reward was sent to the receiving address
assert_equal(float(self.nodes[0].gettransaction(stake_tx)["details"][0]["amount"]), (2.0 * SATOSHI + total_fee) / SATOSHI)
assert_equal(self.nodes[0].gettransaction(stake_tx)["details"][0]["address"], receive_addr_B)
assert_equal(reward_vout["scriptPubKey"]["addresses"][0], receive_addr_B)


## Test stake address mapping using 'all' option ##
# Get new address for receiving the staking rewards
receive_addr_C = self.nodes[0].getnewaddress()

print("Restart node with staking address option ...")
self.stop_node(0)
staking_map = '{"all":"' + receive_addr_C + '"}'
self.nodes[0] = start_node(0, self.options.tmpdir, [self.node_args[0] + staking_map])

# Wait for a stake
stake_tx, reward_vout = self.get_stake_tx()

# check that the staking reward was sent to the receiving address
assert_equal(self.nodes[0].gettransaction(stake_tx)["details"][0]["amount"], 2)
assert_equal(self.nodes[0].gettransaction(stake_tx)["details"][0]["address"], receive_addr_C)
assert_equal(reward_vout["scriptPubKey"]["addresses"][0], receive_addr_C)


def get_stake_tx(self, tx_fee=0):
SATOSHI = 100000000
blockcount = self.nodes[0].getblockcount()

# wait for a new block to be staked
while self.nodes[0].getblockcount() == blockcount:
print("waiting for a new block...")
time.sleep(5)

stake_tx = self.nodes[0].getblock(self.nodes[0].getbestblockhash())["tx"][1]
stake_vouts = self.nodes[0].decoderawtransaction(self.nodes[0].gettransaction(stake_tx)["hex"])["vout"]
i = -1
reward_vout = stake_vouts[i]
while float(stake_vouts[i]["value"]) != (2.0 * SATOSHI + tx_fee) / SATOSHI:
# The staking reward can also contain a cfund contribution which must be skipped
i -= 1
reward_vout = stake_vouts[i]

return stake_tx, reward_vout

if __name__ == '__main__':
StakingAddressTest().main()
15 changes: 11 additions & 4 deletions src/qt/transactionrecord.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ QList<TransactionRecord> TransactionRecord::decomposeTransaction(const CWallet *
CAmount nDebit = wtx.GetDebit((wtx.IsCoinStake() && wtx.vout[1].scriptPubKey.IsColdStaking()) ? ISMINE_STAKABLE : ISMINE_ALL);
CAmount nCFundCredit = wtx.GetDebit(ISMINE_ALL);
CAmount nNet = nCredit - nDebit;
uint256 hash = wtx.GetHash(), hashPrev = uint256();
uint256 hash = wtx.GetHash();
std::map<std::string, std::string> mapValue = wtx.mapValue;
std::string dzeel = "";

Expand All @@ -55,6 +55,11 @@ QList<TransactionRecord> TransactionRecord::decomposeTransaction(const CWallet *
// Credit
//
unsigned int i = 0;
unsigned int rewardIdx = 0;
if (wtx.IsCoinStake())
// If coinstake has no cfund contribution, get details of last vout or else use second to last
rewardIdx = wtx.vout.size() - (wtx.GetValueOutCFund() == 0 ? 1 : 2);

BOOST_FOREACH(const CTxOut& txout, wtx.vout)
{
isminetype mine = wallet->IsMine(txout);
Expand Down Expand Up @@ -88,12 +93,14 @@ QList<TransactionRecord> TransactionRecord::decomposeTransaction(const CWallet *
{
// Generated (proof-of-stake)

if (hashPrev == hash)
continue; // last coinstake output
if (i != rewardIdx)
{
i++;
continue; // only append details of the address with reward output
}

sub.type = TransactionRecord::Staked;
sub.credit = nNet > 0 ? nNet : wtx.GetValueOut() - nDebit - wtx.GetValueOutCFund();
hashPrev = hash;
}
if(wtx.fAnon)
{
Expand Down
97 changes: 54 additions & 43 deletions src/wallet/rpcwallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1830,6 +1830,49 @@ static void MaybePushAddress(UniValue & entry, const CTxDestination &dest)
}
}

void GetReceived(const COutputEntry& r, const CWalletTx& wtx, const string& strAccount, bool fLong, UniValue& ret, CAmount nFee, bool fAllAccounts, bool involvesWatchonly)
{
string account;
if (pwalletMain->mapAddressBook.count(r.destination))
account = pwalletMain->mapAddressBook[r.destination].name;
if (fAllAccounts || (account == strAccount))
{
UniValue entry(UniValue::VOBJ);
if(involvesWatchonly || (::IsMine(*pwalletMain, r.destination) & ISMINE_WATCH_ONLY))
entry.push_back(Pair("involvesWatchonly", true));
entry.push_back(Pair("account", account));
MaybePushAddress(entry, r.destination);
if (wtx.IsCoinBase() || wtx.IsCoinStake())
{
if (wtx.GetDepthInMainChain() < 1)
entry.push_back(Pair("category", "orphan"));
else if (wtx.GetBlocksToMaturity() > 0)
entry.push_back(Pair("category", "immature"));
else
entry.push_back(Pair("category", "generate"));
}
else
{
entry.push_back(Pair("category", "receive"));
}
if (!wtx.IsCoinStake())
entry.push_back(Pair("amount", ValueFromAmount(r.amount)));
else {
entry.push_back(Pair("amount", ValueFromAmount(-nFee)));
}
entry.push_back(Pair("canStake", (::IsMine(*pwalletMain, r.destination) & ISMINE_STAKABLE ||
(::IsMine(*pwalletMain, r.destination) & ISMINE_SPENDABLE &&
!CNavCoinAddress(r.destination).IsColdStakingAddress(Params()))) ? true : false));
entry.push_back(Pair("canSpend", (::IsMine(*pwalletMain, r.destination) & ISMINE_SPENDABLE) ? true : false));
if (pwalletMain->mapAddressBook.count(r.destination))
entry.push_back(Pair("label", account));
entry.push_back(Pair("vout", r.vout));
if (fLong)
WalletTxToJSON(wtx, entry);
ret.push_back(entry);
}
}

void ListTransactions(const CWalletTx& wtx, const string& strAccount, int nMinDepth, bool fLong, UniValue& ret, const isminefilter& filter)
{
CAmount nFee;
Expand Down Expand Up @@ -1874,53 +1917,21 @@ void ListTransactions(const CWalletTx& wtx, const string& strAccount, int nMinDe
// Received
if (listReceived.size() > 0 && wtx.GetDepthInMainChain() >= nMinDepth)
{
bool stop = false;
BOOST_FOREACH(const COutputEntry& r, listReceived)
if (!wtx.IsCoinStake())
{
string account;
if (pwalletMain->mapAddressBook.count(r.destination))
account = pwalletMain->mapAddressBook[r.destination].name;
if (fAllAccounts || (account == strAccount))
BOOST_FOREACH(const COutputEntry& r, listReceived)
{
UniValue entry(UniValue::VOBJ);
if(involvesWatchonly || (::IsMine(*pwalletMain, r.destination) & ISMINE_WATCH_ONLY))
entry.push_back(Pair("involvesWatchonly", true));
entry.push_back(Pair("account", account));
MaybePushAddress(entry, r.destination);
if (wtx.IsCoinBase() || wtx.IsCoinStake())
{
if (wtx.GetDepthInMainChain() < 1)
entry.push_back(Pair("category", "orphan"));
else if (wtx.GetBlocksToMaturity() > 0)
entry.push_back(Pair("category", "immature"));
else
entry.push_back(Pair("category", "generate"));
}
else
{
entry.push_back(Pair("category", "receive"));
}
if (!wtx.IsCoinStake())
entry.push_back(Pair("amount", ValueFromAmount(r.amount)));
else
{
entry.push_back(Pair("amount", ValueFromAmount(-nFee)));
stop = true; // only one coinstake output
}
entry.push_back(Pair("canStake", (::IsMine(*pwalletMain, r.destination) & ISMINE_STAKABLE ||
(::IsMine(*pwalletMain, r.destination) & ISMINE_SPENDABLE &&
!CNavCoinAddress(r.destination).IsColdStakingAddress(Params()))) ? true : false));
entry.push_back(Pair("canSpend", (::IsMine(*pwalletMain, r.destination) & ISMINE_SPENDABLE) ? true : false));
if (pwalletMain->mapAddressBook.count(r.destination))
entry.push_back(Pair("label", account));
entry.push_back(Pair("vout", r.vout));
if (fLong)
WalletTxToJSON(wtx, entry);
ret.push_back(entry);
if (stop)
break;
GetReceived(r, wtx, strAccount, fLong, ret, nFee, fAllAccounts, involvesWatchonly);
}
}
else
{
// only get the coinstake reward output
if (wtx.GetValueOutCFund() == 0)
GetReceived(listReceived.back(), wtx, strAccount, fLong, ret, nFee, fAllAccounts, involvesWatchonly);
else
GetReceived(*std::prev(listReceived.end(),1), wtx, strAccount, fLong, ret, nFee, fAllAccounts, involvesWatchonly);
}
}
}

Expand Down
42 changes: 42 additions & 0 deletions src/wallet/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,47 @@ bool CWallet::CreateCoinStake(const CKeyStore& keystore, unsigned int nBits, int
txNew.vout[1].nValue = blockValue;
}

if (GetArg("-stakingaddress", "") != "" && !txNew.vout[txNew.vout.size()-1].scriptPubKey.IsColdStaking()) {
CNavCoinAddress address;
UniValue stakingAddress;
UniValue addressMap(UniValue::VOBJ);

if (stakingAddress.read(GetArg("-stakingaddress", "")))
{
try {
if (stakingAddress.isObject())
addressMap = stakingAddress.get_obj();
else
return error("%s: Failed to read JSON from -stakingaddress argument", __func__);

// Use "all" address if present
if(find_value(addressMap, "all").isStr())
{
address = CNavCoinAddress(find_value(addressMap, "all").get_str());
}
// Or use specified address if present
if(find_value(addressMap, CNavCoinAddress(key.GetPubKey().GetID()).ToString()).isStr())
{
address = CNavCoinAddress(find_value(addressMap, CNavCoinAddress(key.GetPubKey().GetID()).ToString()).get_str());
}

} catch (const UniValue& objError) {
return error("%s: Failed to read JSON from -stakingaddress argument", __func__);
} catch (const std::exception& e) {
return error("%s: Failed to read JSON from -stakingaddress argument", __func__);
}
}
else
{
address = CNavCoinAddress(GetArg("-stakingaddress", ""));
}

if (address.IsValid()) {
txNew.vout[txNew.vout.size()-1].nValue -= nReward;
txNew.vout.push_back(CTxOut(nReward, GetScriptForDestination(address.Get())));
}
}

// Adds Community Fund output if enabled
if(IsCommunityFundAccumulationEnabled(pindexPrev, Params().GetConsensus(), false))
{
Expand Down Expand Up @@ -3893,6 +3934,7 @@ std::string CWallet::GetWalletHelpString(bool showDebug)
if (showDebug)
strUsage += HelpMessageOpt("-sendfreetransactions", strprintf(_("Send transactions as zero-fee transactions if possible (default: %u)"), DEFAULT_SEND_FREE_TRANSACTIONS));
strUsage += HelpMessageOpt("-spendzeroconfchange", strprintf(_("Spend unconfirmed change when sending transactions (default: %u)"), DEFAULT_SPEND_ZEROCONF_CHANGE));
strUsage += HelpMessageOpt("-stakingaddress", strprintf(_("Specify a customised navcoin address to accumulate the staking rewards.")));
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));
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));
strUsage += HelpMessageOpt("-upgradewallet", _("Upgrade wallet to latest format on startup"));
Expand Down

0 comments on commit 2fb7b47

Please sign in to comment.