Skip to content

Commit a60c39a

Browse files
committed
test: add functional test for addresses and deprecated service field
1 parent e0d2a81 commit a60c39a

File tree

2 files changed

+241
-0
lines changed

2 files changed

+241
-0
lines changed

test/functional/rpc_netinfo.py

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@
233233
'p2p_addrfetch.py',
234234
'rpc_net.py --v1transport',
235235
'rpc_net.py --v2transport',
236+
'rpc_netinfo.py',
236237
'wallet_keypool.py --legacy-wallet',
237238
'wallet_keypool_hd.py --legacy-wallet',
238239
'wallet_keypool_hd.py --descriptors',

0 commit comments

Comments
 (0)