|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (c) 2025 The Dash 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 | +"""Test network information fields across RPCs.""" |
| 6 | + |
| 7 | +from test_framework.util import ( |
| 8 | + assert_equal |
| 9 | +) |
| 10 | +from test_framework.script import ( |
| 11 | + hash160 |
| 12 | +) |
| 13 | +from test_framework.test_framework import ( |
| 14 | + BitcoinTestFramework, |
| 15 | + MasternodeInfo, |
| 16 | + p2p_port |
| 17 | +) |
| 18 | +from test_framework.test_node import TestNode |
| 19 | + |
| 20 | +from _decimal import Decimal |
| 21 | +from random import randint |
| 22 | + |
| 23 | +# See CRegTestParams in src/chainparams.cpp |
| 24 | +DEFAULT_PORT_PLATFORM_P2P = 22200 |
| 25 | +DEFAULT_PORT_PLATFORM_HTTP = 22201 |
| 26 | + |
| 27 | +class Node: |
| 28 | + mn: MasternodeInfo |
| 29 | + node: TestNode |
| 30 | + platform_nodeid: str = "" |
| 31 | + |
| 32 | + def __init__(self, node: TestNode, is_evo: bool): |
| 33 | + self.mn = MasternodeInfo(evo=is_evo, legacy=False) |
| 34 | + self.mn.generate_addresses(node) |
| 35 | + self.mn.set_node(node.index) |
| 36 | + self.mn.set_params(nodePort=p2p_port(node.index)) |
| 37 | + self.node = node |
| 38 | + |
| 39 | + def generate_collateral(self, test: BitcoinTestFramework): |
| 40 | + assert self.mn.nodeIdx is not None |
| 41 | + |
| 42 | + while self.node.getbalance() < self.mn.get_collateral_value(): |
| 43 | + test.bump_mocktime(1) |
| 44 | + test.generate(self.node, 10, sync_fun=test.no_op) |
| 45 | + |
| 46 | + collateral_txid = self.node.sendmany("", {self.mn.collateral_address: self.mn.get_collateral_value(), self.mn.fundsAddr: 1}) |
| 47 | + self.mn.bury_tx(test, self.mn.nodeIdx, collateral_txid, 1) |
| 48 | + collateral_vout = self.mn.get_collateral_vout(self.node, collateral_txid) |
| 49 | + self.mn.set_params(collateral_txid=collateral_txid, collateral_vout=collateral_vout) |
| 50 | + |
| 51 | + def is_mn_visible(self, _protx_hash = None) -> bool: |
| 52 | + protx_hash = _protx_hash or self.mn.proTxHash |
| 53 | + mn_list = self.node.masternodelist() |
| 54 | + mn_visible = False |
| 55 | + for mn_entry in mn_list: |
| 56 | + dmn = mn_list.get(mn_entry) |
| 57 | + if dmn['proTxHash'] == protx_hash: |
| 58 | + assert_equal(dmn['type'], "Evo" if self.mn.evo else "Regular") |
| 59 | + mn_visible = True |
| 60 | + return mn_visible |
| 61 | + |
| 62 | + def register_mn(self, test: BitcoinTestFramework, submit: bool, addrs_core_p2p, addrs_platform_p2p = None, addrs_platform_http = None) -> str: |
| 63 | + assert self.mn.nodeIdx is not None |
| 64 | + |
| 65 | + if self.mn.evo and (not addrs_platform_http or not addrs_platform_p2p): |
| 66 | + raise AssertionError("EvoNode but platformP2PPort and platformHTTPPort not specified") |
| 67 | + |
| 68 | + # Evonode-specific fields are ignored if regular masternode |
| 69 | + self.platform_nodeid = hash160(b'%d' % randint(1, 65535)).hex() |
| 70 | + protx_output = self.mn.register(self.node, submit=submit, coreP2PAddrs=addrs_core_p2p, operator_reward=0, |
| 71 | + platform_node_id=self.platform_nodeid, platform_p2p_port=addrs_platform_p2p, |
| 72 | + platform_http_port=addrs_platform_http) |
| 73 | + assert protx_output is not None |
| 74 | + |
| 75 | + if not submit: |
| 76 | + return "" |
| 77 | + |
| 78 | + # Bury ProTx transaction and check if masternode is online |
| 79 | + self.mn.set_params(proTxHash=protx_output, operator_reward=0) |
| 80 | + self.mn.bury_tx(test, self.mn.nodeIdx, protx_output, 1) |
| 81 | + assert_equal(self.is_mn_visible(), True) |
| 82 | + |
| 83 | + test.log.debug(f"Registered {'Evo' if self.mn.evo else 'regular'} masternode with collateral_txid={self.mn.collateral_txid}, " |
| 84 | + f"collateral_vout={self.mn.collateral_vout}, provider_txid={self.mn.proTxHash}") |
| 85 | + |
| 86 | + test.restart_node(self.mn.nodeIdx, extra_args=self.node.extra_args + [f'-masternodeblsprivkey={self.mn.keyOperator}']) |
| 87 | + return self.mn.proTxHash |
| 88 | + |
| 89 | + def update_mn(self, test: BitcoinTestFramework, addrs_core_p2p, addrs_platform_p2p = None, addrs_platform_http = None) -> str: |
| 90 | + assert self.mn.nodeIdx is not None |
| 91 | + |
| 92 | + if self.mn.evo and (not addrs_platform_http or not addrs_platform_p2p): |
| 93 | + raise AssertionError("EvoNode but platformP2PPort and platformHTTPPort not specified") |
| 94 | + |
| 95 | + # Evonode-specific fields are ignored if regular masternode |
| 96 | + protx_output = self.mn.update_service(self.node, submit=True, coreP2PAddrs=addrs_core_p2p, platform_node_id=self.platform_nodeid, |
| 97 | + platform_p2p_port=addrs_platform_p2p, platform_http_port=addrs_platform_http) |
| 98 | + assert protx_output is not None |
| 99 | + |
| 100 | + self.mn.bury_tx(test, self.mn.nodeIdx, protx_output, 1) |
| 101 | + assert_equal(self.is_mn_visible(), True) |
| 102 | + |
| 103 | + test.log.debug(f"Updated {'Evo' if self.mn.evo else 'regular'} masternode with collateral_txid={self.mn.collateral_txid}, " |
| 104 | + f"collateral_vout={self.mn.collateral_vout}, provider_txid={self.mn.proTxHash}") |
| 105 | + return protx_output |
| 106 | + |
| 107 | + def destroy_mn(self, test: BitcoinTestFramework): |
| 108 | + assert self.mn.nodeIdx is not None |
| 109 | + |
| 110 | + # Get UTXO from address used to pay fees, generate new addresses |
| 111 | + address_funds_unspent = self.node.listunspent(0, 99999, [self.mn.fundsAddr])[0] |
| 112 | + address_funds_value = address_funds_unspent['amount'] |
| 113 | + self.mn.generate_addresses(self.node, True) |
| 114 | + |
| 115 | + # Create transaction to spend old collateral and fee change |
| 116 | + raw_tx = self.node.createrawtransaction([ |
| 117 | + { 'txid': self.mn.collateral_txid, 'vout': self.mn.collateral_vout }, |
| 118 | + { 'txid': address_funds_unspent['txid'], 'vout': address_funds_unspent['vout'] } |
| 119 | + ], [ |
| 120 | + {self.mn.collateral_address: float(self.mn.get_collateral_value())}, |
| 121 | + {self.mn.fundsAddr: float(address_funds_value - Decimal(0.001))} |
| 122 | + ]) |
| 123 | + raw_tx = self.node.signrawtransactionwithwallet(raw_tx)['hex'] |
| 124 | + |
| 125 | + # Send that transaction, resulting txid is new collateral |
| 126 | + new_collateral_txid = self.node.sendrawtransaction(raw_tx) |
| 127 | + self.mn.bury_tx(test, self.mn.nodeIdx, new_collateral_txid, 1) |
| 128 | + new_collateral_vout = self.mn.get_collateral_vout(self.node, new_collateral_txid) |
| 129 | + self.mn.set_params(proTxHash="", collateral_txid=new_collateral_txid, collateral_vout=new_collateral_vout) |
| 130 | + |
| 131 | + # Old masternode entry should be dead |
| 132 | + assert_equal(self.is_mn_visible(self.mn.proTxHash), False) |
| 133 | + test.log.debug(f"Destroyed {'Evo' if self.mn.evo else 'regular'} masternode with collateral_txid={self.mn.collateral_txid}, " |
| 134 | + f"collateral_vout={self.mn.collateral_vout}, provider_txid={self.mn.proTxHash}") |
| 135 | + |
| 136 | + test.restart_node(self.mn.nodeIdx, extra_args=self.node.extra_args) |
| 137 | + |
| 138 | +class NetInfoTest(BitcoinTestFramework): |
| 139 | + def set_test_params(self): |
| 140 | + self.num_nodes = 2 |
| 141 | + self.extra_args = [ |
| 142 | + ["-dip3params=2:2"], |
| 143 | + ["-deprecatedrpc=service", "-dip3params=2:2"] |
| 144 | + ] |
| 145 | + |
| 146 | + def skip_test_if_missing_module(self): |
| 147 | + self.skip_if_no_wallet() |
| 148 | + |
| 149 | + def check_netinfo_fields(self, val, core_p2p_port: int): |
| 150 | + assert_equal(val[0], f"127.0.0.1:{core_p2p_port}") |
| 151 | + |
| 152 | + def run_test(self): |
| 153 | + self.node_evo: Node = Node(self.nodes[0], True) |
| 154 | + self.node_evo.generate_collateral(self) |
| 155 | + |
| 156 | + self.node_simple: TestNode = self.nodes[1] |
| 157 | + |
| 158 | + # netInfo is represented with JSON in CProRegTx, CProUpServTx, CDeterministicMNState and CSimplifiedMNListEntry, |
| 159 | + # so we need to test calls that rely on these underlying implementations. Start by collecting RPC responses. |
| 160 | + self.log.info("Collect JSON RPC responses from node") |
| 161 | + |
| 162 | + # CProRegTx::ToJson() <- TxToUniv() <- TxToJSON() <- getrawtransaction |
| 163 | + proregtx_hash = self.node_evo.register_mn(self, True, f"127.0.0.1:{self.node_evo.mn.nodePort}", DEFAULT_PORT_PLATFORM_P2P, DEFAULT_PORT_PLATFORM_HTTP) |
| 164 | + proregtx_rpc = self.node_evo.node.getrawtransaction(proregtx_hash, True) |
| 165 | + |
| 166 | + # CDeterministicMNState::ToJson() <- CDeterministicMN::pdmnState <- masternode_status |
| 167 | + masternode_status = self.node_evo.node.masternode('status') |
| 168 | + |
| 169 | + # Generate deprecation-disabled response to avoid having to re-create a masternode again later on |
| 170 | + self.restart_node(self.node_evo.mn.nodeIdx, extra_args=self.node_evo.node.extra_args + |
| 171 | + [f'-masternodeblsprivkey={self.node_evo.mn.keyOperator}', '-deprecatedrpc=service']) |
| 172 | + self.connect_nodes(self.node_evo.mn.nodeIdx, self.node_simple.index) # Needed as restarts don't reconnect nodes |
| 173 | + masternode_status_depr = self.node_evo.node.masternode('status') |
| 174 | + |
| 175 | + # Stop actively running the masternode so we can issue a CProUpServTx (and enable the deprecation) |
| 176 | + self.restart_node(self.node_evo.mn.nodeIdx, extra_args=self.node_evo.node.extra_args) |
| 177 | + self.connect_nodes(self.node_evo.mn.nodeIdx, self.node_simple.index) # Needed as restarts don't reconnect nodes |
| 178 | + |
| 179 | + # CProUpServTx::ToJson() <- TxToUniv() <- TxToJSON() <- getrawtransaction |
| 180 | + proupservtx_hash = self.node_evo.update_mn(self, f"127.0.0.1:{self.node_evo.mn.nodePort+1}", DEFAULT_PORT_PLATFORM_P2P, DEFAULT_PORT_PLATFORM_HTTP) |
| 181 | + proupservtx_rpc = self.node_evo.node.getrawtransaction(proupservtx_hash, True) |
| 182 | + |
| 183 | + # We need to update *twice*, the first time to incorrect values and the second time, back to correct values. |
| 184 | + # This is to make sure that the fields we need to check against are reflected in the diff. |
| 185 | + proupservtx_hash = self.node_evo.update_mn(self, f"127.0.0.1:{self.node_evo.mn.nodePort}", DEFAULT_PORT_PLATFORM_P2P, DEFAULT_PORT_PLATFORM_HTTP) |
| 186 | + proupservtx_rpc = self.node_evo.node.getrawtransaction(proupservtx_hash, True) |
| 187 | + |
| 188 | + # CSimplifiedMNListEntry::ToJson() <- CSimplifiedMNListDiff::mnList <- CSimplifiedMNListDiff::ToJson() <- protx_diff |
| 189 | + masternode_active_height: int = masternode_status['dmnState']['registeredHeight'] |
| 190 | + protx_diff_rpc = self.node_evo.node.protx('diff', masternode_active_height - 1, masternode_active_height) |
| 191 | + |
| 192 | + # CDeterministicMNStateDiff::ToJson() <- CDeterministicMNListDiff::updatedMns <- protx_listdiff |
| 193 | + proupservtx_height = proupservtx_rpc['height'] |
| 194 | + protx_listdiff_rpc = self.node_evo.node.protx('listdiff', proupservtx_height - 1, proupservtx_height) |
| 195 | + |
| 196 | + self.log.info("Test RPCs return an 'addresses' field") |
| 197 | + assert "addresses" in proregtx_rpc['proRegTx'].keys() |
| 198 | + assert "addresses" in masternode_status['dmnState'].keys() |
| 199 | + assert "addresses" in proupservtx_rpc['proUpServTx'].keys() |
| 200 | + assert "addresses" in protx_diff_rpc['mnList'][0].keys() |
| 201 | + assert "addresses" in protx_listdiff_rpc['updatedMNs'][0][proregtx_hash].keys() |
| 202 | + |
| 203 | + self.log.info("Test 'addresses' report correctly") |
| 204 | + self.check_netinfo_fields(proregtx_rpc['proRegTx']['addresses'], self.node_evo.mn.nodePort) |
| 205 | + self.check_netinfo_fields(masternode_status['dmnState']['addresses'], self.node_evo.mn.nodePort) |
| 206 | + self.check_netinfo_fields(proupservtx_rpc['proUpServTx']['addresses'], self.node_evo.mn.nodePort) |
| 207 | + self.check_netinfo_fields(protx_diff_rpc['mnList'][0]['addresses'], self.node_evo.mn.nodePort) |
| 208 | + self.check_netinfo_fields(protx_listdiff_rpc['updatedMNs'][0][proregtx_hash]['addresses'], self.node_evo.mn.nodePort) |
| 209 | + |
| 210 | + self.log.info("Test RPCs by default no longer return a 'service' field") |
| 211 | + assert "service" not in proregtx_rpc['proRegTx'].keys() |
| 212 | + assert "service" not in masternode_status['dmnState'].keys() |
| 213 | + assert "service" not in proupservtx_rpc['proUpServTx'].keys() |
| 214 | + assert "service" not in protx_diff_rpc['mnList'][0].keys() |
| 215 | + assert "service" not in protx_listdiff_rpc['updatedMNs'][0][proregtx_hash].keys() |
| 216 | + # "service" in "masternode status" is exempt from the deprecation as the primary address is |
| 217 | + # relevant on the host node as opposed to expressing payload information in most other RPCs. |
| 218 | + assert "service" in masternode_status.keys() |
| 219 | + |
| 220 | + self.node_evo.destroy_mn(self) # Shut down previous masternode |
| 221 | + self.connect_nodes(self.node_evo.mn.nodeIdx, self.node_simple.index) # Needed as restarts don't reconnect nodes |
| 222 | + |
| 223 | + self.log.info("Collect RPC responses from node with -deprecatedrpc=service") |
| 224 | + |
| 225 | + # Re-use chain activity from earlier |
| 226 | + proregtx_rpc = self.node_simple.getrawtransaction(proregtx_hash, True) |
| 227 | + proupservtx_rpc = self.node_simple.getrawtransaction(proupservtx_hash, True) |
| 228 | + protx_diff_rpc = self.node_simple.protx('diff', masternode_active_height - 1, masternode_active_height) |
| 229 | + masternode_status = masternode_status_depr # Pull in response generated from earlier |
| 230 | + protx_listdiff_rpc = self.node_simple.protx('listdiff', proupservtx_height - 1, proupservtx_height) |
| 231 | + |
| 232 | + self.log.info("Test RPCs return 'service' with -deprecatedrpc=service") |
| 233 | + assert "service" in proregtx_rpc['proRegTx'].keys() |
| 234 | + assert "service" in masternode_status['dmnState'].keys() |
| 235 | + assert "service" in proupservtx_rpc['proUpServTx'].keys() |
| 236 | + assert "service" in protx_diff_rpc['mnList'][0].keys() |
| 237 | + assert "service" in protx_listdiff_rpc['updatedMNs'][0][proregtx_hash].keys() |
| 238 | + |
| 239 | +if __name__ == "__main__": |
| 240 | + NetInfoTest().main() |
0 commit comments