Skip to content

Commit d3d1250

Browse files
authored
Exclude spent outputs from remote staking balance (dtr-org#1000)
At the moment, WalletExtension::GetRemoteStakingBalance counts every output in remote staking coinbase and shows incorrect data in getwalletinfo response. This change excludes spent outputs from remote staking balance. * Ignore spent transaction in WalletExtension::GetRemoteStakingBalance * Don't count spent outputs in GetRemoteStakingBalance * Fix nits * Check actual balance unchanged * Add a test case for remote stake spending Signed-off-by: Dmitry Saveliev <dima@thirdhash.com>
1 parent 94f2e32 commit d3d1250

File tree

3 files changed

+110
-18
lines changed

3 files changed

+110
-18
lines changed

src/esperanza/walletextension.cpp

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,16 @@ CAmount WalletExtension::GetRemoteStakingBalance() const {
121121
CAmount balance = 0;
122122

123123
for (const auto &it : m_enclosing_wallet.mapWallet) {
124-
const CWalletTx *const tx = &it.second;
124+
const CWalletTx &tx = it.second;
125+
const uint256 &tx_hash = tx.GetHash();
125126

126-
for (const auto &txout : tx->tx->vout) {
127-
if (::IsStakedRemotely(m_enclosing_wallet, txout.scriptPubKey)) {
128-
balance += txout.nValue;
127+
for (size_t i = 0; i < tx.tx->vout.size(); ++i) {
128+
const CTxOut &tx_out = tx.tx->vout[i];
129+
if (m_enclosing_wallet.IsSpent(tx_hash, i)) {
130+
continue;
131+
}
132+
if (::IsStakedRemotely(m_enclosing_wallet, tx_out.scriptPubKey)) {
133+
balance += tx_out.nValue;
129134
}
130135
}
131136
}

test/functional/feature_remote_staking.py

Lines changed: 98 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,36 @@
22
# Copyright (c) 2019 The Unit-e 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-
from test_framework.mininode import sha256
5+
from test_framework.blocktools import (
6+
create_block,
7+
create_coinbase,
8+
get_tip_snapshot_meta,
9+
sign_coinbase,
10+
)
11+
from test_framework.mininode import (
12+
msg_witness_block,
13+
network_thread_start,
14+
P2PInterface,
15+
sha256,
16+
)
617
from test_framework.regtest_mnemonics import regtest_mnemonics
7-
from test_framework.script import CScript, OP_2, hash160
8-
from test_framework.test_framework import UnitETestFramework, STAKE_SPLIT_THRESHOLD
9-
from test_framework.util import assert_equal, assert_greater_than, bytes_to_hex_str, hex_str_to_bytes, wait_until
18+
from test_framework.script import (
19+
CScript,
20+
OP_2,
21+
hash160,
22+
)
23+
from test_framework.test_framework import (
24+
UnitETestFramework,
25+
PROPOSER_REWARD,
26+
STAKE_SPLIT_THRESHOLD,
27+
)
28+
from test_framework.util import (
29+
assert_equal,
30+
assert_greater_than,
31+
bytes_to_hex_str,
32+
hex_str_to_bytes,
33+
wait_until,
34+
)
1035

1136

1237
def stake_p2wsh(node, staking_node, amount):
@@ -18,23 +43,49 @@ def stake_p2wsh(node, staking_node, amount):
1843
staking_node: the node which will be able to stake nodes
1944
amount: the amount to send
2045
"""
21-
multisig = node.addmultisigaddress(2, [node.getnewaddress(), node.getnewaddress()])
46+
multisig = node.addmultisigaddress(
47+
2, [node.getnewaddress(), node.getnewaddress()])
2248
bare = CScript(hex_str_to_bytes(multisig['redeemScript']))
2349
spending_script_hash = sha256(bare)
2450

25-
addr_info = staking_node.validateaddress(staking_node.getnewaddress('', 'legacy'))
51+
addr_info = staking_node.validateaddress(
52+
staking_node.getnewaddress('', 'legacy'))
2653
staking_key_hash = hash160(hex_str_to_bytes(addr_info['pubkey']))
2754

2855
rs_p2wsh = CScript([OP_2, staking_key_hash, spending_script_hash])
29-
outputs = [{'address': 'script', 'amount': amount, 'script': bytes_to_hex_str(rs_p2wsh)}]
30-
node.sendtypeto('unite', 'unite', outputs)
56+
outputs = [{'address': 'script', 'amount': amount,
57+
'script': bytes_to_hex_str(rs_p2wsh)}]
58+
return node.sendtypeto('unite', 'unite', outputs)
59+
60+
61+
def build_block_with_remote_stake(node):
62+
height = node.getblockcount()
63+
snapshot_meta = get_tip_snapshot_meta(node)
64+
stakes = node.liststakeablecoins()
65+
66+
coin = stakes['stakeable_coins'][0]['coin']
67+
script_pubkey = hex_str_to_bytes(coin['script_pub_key']['hex'])
68+
stake = {
69+
'txid': coin['out_point']['txid'],
70+
'vout': coin['out_point']['n'],
71+
'amount': coin['amount'],
72+
}
73+
74+
tip = int(node.getbestblockhash(), 16)
75+
block_time = node.getblock(
76+
node.getbestblockhash())['time'] + 1
77+
coinbase = sign_coinbase(
78+
node, create_coinbase(
79+
height, stake, snapshot_meta.hash, raw_script_pubkey=script_pubkey))
80+
81+
return create_block(tip, coinbase, block_time)
3182

3283

3384
class RemoteStakingTest(UnitETestFramework):
3485
def set_test_params(self):
3586
self.num_nodes = 2
3687
self.setup_clean_chain = True
37-
self.extra_args=[
88+
self.extra_args = [
3889
[],
3990
['-minimumchainwork=0', '-maxtipage=1000000000']
4091
]
@@ -43,8 +94,13 @@ def run_test(self):
4394
alice, bob = self.nodes
4495
alice.importmasterkey(regtest_mnemonics[0]['mnemonics'])
4596

97+
bob.add_p2p_connection(P2PInterface())
98+
network_thread_start()
99+
bob.p2p.wait_for_verack()
100+
46101
alice.generate(1)
47-
assert_equal(len(alice.listunspent()), regtest_mnemonics[0]['balance'] / STAKE_SPLIT_THRESHOLD)
102+
assert_equal(len(alice.listunspent()),
103+
regtest_mnemonics[0]['balance'] / STAKE_SPLIT_THRESHOLD)
48104

49105
alices_addr = alice.getnewaddress()
50106

@@ -60,21 +116,50 @@ def run_test(self):
60116
assert_equal(ps['wallets'][0]['stakeable_balance'], 0)
61117

62118
# Stake the funds
63-
result = alice.stakeat(recipient)
64-
stake_p2wsh(alice, staking_node=bob, amount=1)
119+
tx1_hash = alice.stakeat(recipient)
120+
tx2_hash = stake_p2wsh(alice, staking_node=bob, amount=1)
65121
alice.generatetoaddress(1, alices_addr)
66122
self.sync_all()
67123

124+
# Estimate Alice balance
125+
tx1_fee = alice.gettransaction(tx1_hash)['fee']
126+
tx2_fee = alice.gettransaction(tx2_hash)['fee']
127+
alice_balance = regtest_mnemonics[0]['balance'] + tx1_fee + tx2_fee
128+
68129
wi = alice.getwalletinfo()
69130
assert_equal(wi['remote_staking_balance'], 2)
131+
assert_equal(wi['balance'], alice_balance)
70132

71133
def bob_is_staking_the_new_coin():
72134
ps = bob.proposerstatus()
73135
return ps['wallets'][0]['stakeable_balance'] == 2
74136
wait_until(bob_is_staking_the_new_coin, timeout=10)
75137

138+
# Bob generates a new block with remote stake of Alice
139+
block = build_block_with_remote_stake(bob)
140+
bob.p2p.send_and_ping(msg_witness_block(block))
141+
self.sync_all()
142+
143+
# Reward from the Bob's block comes to remote staking balance of Alice
144+
# Actual spendable balance shouldn't change because the reward is not yet mature
145+
wi = alice.getwalletinfo()
146+
assert_equal(wi['remote_staking_balance'], 2 + PROPOSER_REWARD)
147+
assert_equal(wi['balance'], alice_balance)
148+
76149
# Change outputs for both staked coins, and the balance staked remotely
77-
assert_equal(len(alice.listunspent()), 2 + (regtest_mnemonics[0]['balance'] // STAKE_SPLIT_THRESHOLD))
150+
assert_equal(len(alice.listunspent()), 2 +
151+
(regtest_mnemonics[0]['balance'] // STAKE_SPLIT_THRESHOLD))
152+
153+
# Chech that Alice can spend remotely staked coins
154+
inputs = []
155+
for coin in bob.liststakeablecoins()['stakeable_coins']:
156+
out_point = coin['coin']['out_point']
157+
inputs.append({'tx': out_point['txid'], 'n': out_point['n']})
158+
alice.sendtypeto('', '', [{'address': alices_addr, 'amount': 1.9}], '', '', False,
159+
{'changeaddress': alices_addr, 'inputs': inputs})
160+
161+
wi = alice.getwalletinfo()
162+
assert_equal(wi['remote_staking_balance'], PROPOSER_REWARD)
78163

79164

80165
if __name__ == '__main__':

test/functional/test_framework/blocktools.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def serialize_script_num(value):
6969
# If pubkey is passed in, the coinbase outputs will be P2PK outputs;
7070
# otherwise anyone-can-spend outputs. The first output is the reward,
7171
# which is not spendable for COINBASE_MATURITY blocks.
72-
def create_coinbase(height, stake, snapshot_hash, pubkey = None, n_pieces = 1):
72+
def create_coinbase(height, stake, snapshot_hash, pubkey=None, raw_script_pubkey=None, n_pieces=1):
7373
assert n_pieces > 0
7474
stake_in = COutPoint(int(stake['txid'], 16), stake['vout'])
7575
coinbase = CTransaction()
@@ -81,6 +81,8 @@ def create_coinbase(height, stake, snapshot_hash, pubkey = None, n_pieces = 1):
8181
output_script = None
8282
if (pubkey != None):
8383
output_script = CScript([pubkey, OP_CHECKSIG])
84+
elif raw_script_pubkey is not None:
85+
output_script = CScript(raw_script_pubkey)
8486
else:
8587
output_script = CScript([OP_TRUE])
8688

0 commit comments

Comments
 (0)