Skip to content

Commit 1512abf

Browse files
committed
test: add functional test for addresses and deprecated service field
1 parent af389f3 commit 1512abf

File tree

2 files changed

+258
-0
lines changed

2 files changed

+258
-0
lines changed

test/functional/rpc_netinfo.py

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