|
| 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 () |
0 commit comments