Skip to content

Commit 7994dbc

Browse files
domob1812galpHub
authored andcommitted
Regtest for proposed mn vaults.
This adds a new regtest mnvaults.py, which manually implements the proposal for masternode vaults with a temporary key and a pre-signed unvault transaction. It sets up a masternode with a collateral at a temporary address, for which we need not retain the private key. Instead we just keep a pre-signed transaction to withdraw the funds to a specified address and the signed masternode broadcast in storage, and use them to first run the masternode and then unvault the funds.
1 parent 1201904 commit 7994dbc

File tree

2 files changed

+272
-0
lines changed

2 files changed

+272
-0
lines changed

divi/qa/rpc-tests/mnvaults.py

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2020 The DIVI developers
3+
# Distributed under the MIT/X11 software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
# Tests the workflow for setting up a masternode vault (with prepared
7+
# unvault tx and destroyed private key), running the masternode with it
8+
# and unvaulting the funds later.
9+
#
10+
# We use seven nodes:
11+
# - node 0 is used to fund and unvault the masternode
12+
# - node 1 is the "hot" masternode
13+
# - node 2 holds the "temporary" vault key and can sign with it
14+
# (but we use it sparingly)
15+
# - nodes 3-6 are just used to get above the "three full nodes" threshold
16+
17+
from test_framework import BitcoinTestFramework
18+
from util import *
19+
from messages import *
20+
from masternode import *
21+
22+
from binascii import unhexlify
23+
import time
24+
25+
26+
class MnVaultsTest (BitcoinTestFramework):
27+
28+
def __init__ (self):
29+
super (MnVaultsTest, self).__init__ ()
30+
self.base_args = ["-debug", "-nolistenonion"]
31+
self.cfg = None
32+
33+
def setup_chain (self):
34+
for i in range (7):
35+
initialize_datadir (self.options.tmpdir, i)
36+
37+
def setup_network (self, config_line=None, extra_args=[]):
38+
# The masternode starts off, the others are online initially.
39+
self.nodes = [
40+
start_node (0, self.options.tmpdir, extra_args=self.base_args),
41+
None,
42+
] + [
43+
start_node (i, self.options.tmpdir, extra_args=self.base_args)
44+
for i in [2, 3, 4, 5, 6]
45+
]
46+
47+
# We want to work with mock times that are beyond the genesis
48+
# block timestamp but before current time (so that nodes being
49+
# started up and before they get on mocktime aren't rejecting
50+
# the on-disk blockchain).
51+
self.time = 1580000000
52+
assert self.time < time.time ()
53+
set_node_times (self.nodes, self.time)
54+
55+
# Nodes 3-5 are connected between each other, and the cluster is
56+
# also connected to nodes 0-2.
57+
connect_nodes (self.nodes[3], 4)
58+
connect_nodes (self.nodes[3], 5)
59+
connect_nodes (self.nodes[3], 6)
60+
connect_nodes (self.nodes[4], 5)
61+
connect_nodes (self.nodes[4], 6)
62+
connect_nodes (self.nodes[5], 6)
63+
for i in [0, 2]:
64+
connect_nodes (self.nodes[i], 3)
65+
connect_nodes (self.nodes[i], 4)
66+
connect_nodes (self.nodes[i], 5)
67+
connect_nodes (self.nodes[i], 6)
68+
69+
self.is_network_split = False
70+
71+
def start_node (self, n):
72+
"""Starts node n with the proper arguments
73+
and masternode config for it."""
74+
75+
args = self.base_args
76+
if n == 1:
77+
args.append ("-masternode")
78+
args.append ("-masternodeprivkey=%s" % self.cfg.privkey)
79+
80+
if self.cfg:
81+
cfg = [self.cfg.line]
82+
else:
83+
cfg = []
84+
85+
self.nodes[n] = start_node (n, self.options.tmpdir,
86+
extra_args=args, mn_config_lines=cfg)
87+
self.nodes[n].setmocktime (self.time)
88+
89+
for i in [3, 4, 5, 6]:
90+
connect_nodes (self.nodes[n], i)
91+
92+
sync_blocks (self.nodes)
93+
94+
def stop_node (self, n):
95+
stop_node (self.nodes[n], n)
96+
self.nodes[n] = None
97+
98+
def advance_time (self, dt=1):
99+
"""Advances mocktime by the given number of seconds."""
100+
101+
self.time += dt
102+
set_node_times (self.nodes, self.time)
103+
104+
def mine_blocks (self, n):
105+
"""Mines blocks with node 3."""
106+
107+
sync_mempools (self.nodes)
108+
self.nodes[3].setgenerate(True, n)
109+
sync_blocks (self.nodes)
110+
111+
def run_test (self):
112+
self.fund_vault ()
113+
self.start_masternode ()
114+
self.get_payments ()
115+
self.unvault ()
116+
117+
def fund_vault (self):
118+
print ("Funding masternode vault...")
119+
120+
self.nodes[0].setgenerate (True, 5)
121+
sync_blocks (self.nodes)
122+
self.mine_blocks (20)
123+
124+
addr = self.nodes[2].getnewaddress ()
125+
privkey = self.nodes[2].dumpprivkey (addr)
126+
127+
amount = 100
128+
txid = self.nodes[0].sendtoaddress (addr, amount)
129+
raw = self.nodes[0].getrawtransaction (txid, 1)
130+
vout = None
131+
for i in range (len (raw["vout"])):
132+
o = raw["vout"][i]
133+
if addr in o["scriptPubKey"]["addresses"]:
134+
vout = i
135+
break
136+
assert vout is not None
137+
138+
unvaultAddr = self.nodes[0].getnewaddress ("unvaulted")
139+
data = self.nodes[0].validateaddress (unvaultAddr)
140+
141+
tx = CTransaction ()
142+
tx.vin.append (CTxIn (COutPoint (txid=txid, n=vout)))
143+
tx.vout.append (CTxOut (amount * COIN, unhexlify (data["scriptPubKey"])))
144+
unsigned = ToHex (tx)
145+
146+
validated = self.nodes[0].validateaddress (addr)
147+
script = validated["scriptPubKey"]
148+
prevtx = [{"txid": txid, "vout": vout, "scriptPubKey": script}]
149+
signed = self.nodes[0].signrawtransaction (unsigned, prevtx, [privkey],
150+
"SINGLE|ANYONECANPAY")
151+
assert_equal (signed["complete"], True)
152+
self.unvaultTx = signed["hex"]
153+
154+
self.cfg = fund_masternode (self.nodes[0], "mn", "copper", txid,
155+
"localhost:%d" % p2p_port (1))
156+
# FIXME: Use reward address from node 0.
157+
self.cfg.rewardAddr = addr
158+
159+
for i in [0, 2]:
160+
self.stop_node (i)
161+
self.start_node (i)
162+
163+
# Prepare the masternode activation broadcast, without actually
164+
# relaying it to the network. After this is done, node 2 with the
165+
# "temporary" private key is no longer needed at all, and can be
166+
# shut down for the rest of the test.
167+
bc = self.nodes[2].startmasternode ("mn", True)
168+
assert_equal (bc["status"], "success")
169+
self.broadcast = bc["broadcastData"]
170+
self.stop_node (2)
171+
172+
self.mine_blocks (20)
173+
174+
def start_masternode (self):
175+
print ("Starting masternode from vault...")
176+
177+
# Advance some time to simulate starting the node later (e.g. also when
178+
# restarting it as necessary during operation).
179+
for _ in range (100):
180+
self.advance_time (100)
181+
182+
# Due to advancing the time without having any masternodes, sync will
183+
# have failed on the nodes that are up. Reset the sync now to make
184+
# sure they will then properly sync together with the other nodes
185+
# after we start our masternode.
186+
for n in self.nodes:
187+
if n is not None:
188+
n.mnsync ("reset")
189+
190+
# Now start and activate the masternode based on the stored
191+
# broadcast message.
192+
self.start_node (1)
193+
bc = self.nodes[1].broadcaststartmasternode (self.broadcast, "update_ping")
194+
assert_equal (bc["status"], "success")
195+
196+
# Finish masternode sync.
197+
for _ in range (100):
198+
self.advance_time ()
199+
for n in self.nodes:
200+
if n is not None:
201+
status = n.mnsync ("status")
202+
assert_equal (status["RequestedMasternodeAssets"], 999)
203+
204+
# Check that the masternode is indeed active.
205+
data = self.nodes[1].getmasternodestatus ()
206+
assert_equal (data["status"], 4)
207+
assert_equal (data["message"], "Masternode successfully started")
208+
209+
def get_payments (self):
210+
print ("Receiving masternode payments...")
211+
212+
# For payments, the masternode needs to be active at least 8000 seconds
213+
# and we also need at least 100 blocks. We also need some extra
214+
# leeway in the time due to the one hour we add to the current time
215+
# when signing a collateral that is not yet 15 times confirmed.
216+
self.mine_blocks (100)
217+
for _ in range (150):
218+
self.advance_time (100)
219+
220+
cnt = self.nodes[3].getmasternodecount ()
221+
assert_equal (cnt["total"], 1)
222+
assert_equal (cnt["enabled"], 1)
223+
assert_equal (cnt["inqueue"], 1)
224+
225+
# Mine some blocks, but advance the time in between and do it
226+
# one by one so the masternode winners can get broadcast between
227+
# blocks and such.
228+
for _ in range (10):
229+
self.mine_blocks (1)
230+
self.advance_time (10)
231+
232+
# Check that some payments were made.
233+
winners = self.nodes[3].getmasternodewinners ()
234+
found = False
235+
for w in winners:
236+
if w["winner"]["address"] == self.cfg.rewardAddr:
237+
found = True
238+
break
239+
assert_equal (found, True)
240+
241+
# FIXME: Check in wallet when we have a custom reward address.
242+
243+
def unvault (self):
244+
print ("Unvaulting the funds...")
245+
246+
# The prepared unvaulting tx is just a single input/output pair
247+
# with no fee attached. To add the transaction fee, we add another
248+
# input and output, which is fine due to the SINGLE|ANYONECANPAY signature
249+
# that we used.
250+
251+
fee = Decimal ('0.10000000')
252+
inp = self.nodes[0].listunspent ()[0]
253+
change = int ((inp["amount"] - fee) * COIN)
254+
assert_greater_than (change, 0)
255+
changeAddr = self.nodes[0].getnewaddress ()
256+
data = self.nodes[0].validateaddress (changeAddr)
257+
258+
tx = FromHex (CTransaction (), self.unvaultTx)
259+
tx.vin.append (CTxIn (COutPoint (txid=inp["txid"], n=inp["vout"])))
260+
tx.vout.append (CTxOut (change, unhexlify (data["scriptPubKey"])))
261+
partial = ToHex (tx)
262+
263+
signed = self.nodes[0].signrawtransaction (partial)
264+
assert_equal (signed["complete"], True)
265+
self.nodes[0].sendrawtransaction (signed["hex"])
266+
self.mine_blocks (1)
267+
assert_equal (self.nodes[0].getbalance ("unvaulted"), 100)
268+
269+
270+
if __name__ == '__main__':
271+
MnVaultsTest ().main ()

divi/qa/rpc-tests/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
'MnAreSafeToRestart.py',
9191
'mncollateral.py',
9292
'mnoperation.py',
93+
'mnvaults.py',
9394
'op_meta.py',
9495
'proxy_test.py',
9596
'receivedby.py',

0 commit comments

Comments
 (0)