Skip to content

Commit 30de9e9

Browse files
author
Andres Correa Casablanca
committed
Isolate workaround nodes from experiment network
Signed-off-by: Andres Correa Casablanca <[email protected]>
1 parent 51e3076 commit 30de9e9

File tree

2 files changed

+142
-82
lines changed

2 files changed

+142
-82
lines changed

experiments/forking_simulation.py

Lines changed: 130 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@
3838
getLogger
3939
)
4040
from math import floor
41-
from os import environ
41+
from os import (
42+
environ,
43+
mkdir
44+
)
4245
from os.path import (
4346
dirname,
4447
exists as path_exists,
@@ -51,6 +54,8 @@
5154
from tempfile import mkdtemp
5255
from time import time as _time
5356
from typing import (
57+
Dict,
58+
Iterable,
5459
List,
5560
Optional,
5661
Set,
@@ -132,7 +137,7 @@ def __init__(
132137

133138
# Required to interact with the network & the nodes
134139
self.loop = loop
135-
self.nodes: List[TestNode] = []
140+
self.nodes: Dict[int, TestNode] = {}
136141
self.nodes_hub: Optional[NodesHub] = None
137142
self.proposer_node_ids: List[int] = []
138143
self.validator_node_ids: List[int] = []
@@ -145,11 +150,18 @@ def run(self) -> bool:
145150
self.setup_chain()
146151
self.setup_nodes()
147152

148-
try:
149-
if self.num_validator_nodes > 0:
153+
if self.num_validator_nodes > 0:
154+
try:
150155
self.autofinalization_workaround()
156+
except BaseException as e:
157+
self.logger.critical(
158+
'Workaround execution failure', exc_info=e
159+
)
160+
return False
161+
try:
151162
self.start_nodes()
152-
except (OSError, AssertionError):
163+
except (OSError, AssertionError) as e:
164+
self.logger.critical('Unable to start nodes', exc_info=e)
153165
return False # Early shutdown
154166

155167
self.nodes_hub = NodesHub(
@@ -163,14 +175,19 @@ def run(self) -> bool:
163175
self.nodes_hub.sync_start_proxies()
164176
self.nodes_hub.sync_connect_nodes_graph(self.graph_edges)
165177

166-
# Notice that the validators have already loaded their wallets
167178
self.logger.info('Importing wallets')
168-
for idx, proposer_id in enumerate(self.proposer_node_ids):
169-
if idx > 0:
170-
self.nodes[proposer_id].createwallet(f'n{proposer_id}')
179+
for node_id, node in self.nodes.items():
180+
node.createwallet(f'n{node_id}')
181+
tmp_wallet = node.get_wallet_rpc(f'n{node_id}')
171182

172-
tmp_wallet = self.nodes[proposer_id].get_wallet_rpc(f'n{proposer_id}')
173-
tmp_wallet.importwallet(normpath(self.tmp_dir + f'/n{proposer_id}.wallet'))
183+
if self.num_validator_nodes > 0:
184+
tmp_wallet.importwallet(
185+
normpath(self.tmp_dir + f'/n{node_id}.wallet')
186+
)
187+
else:
188+
tmp_wallet.importmasterkey(
189+
regtest_mnemonics[node_id]['mnemonics']
190+
)
174191

175192
self.loop.run_until_complete(self.trigger_simulation_stop())
176193
return True
@@ -183,10 +200,20 @@ def autofinalization_workaround(self):
183200
self.logger.info('Running auto-finalization workaround')
184201

185202
lucky_proposer_id = self.proposer_node_ids[0]
186-
validators = [self.nodes[i] for i in self.validator_node_ids]
203+
lucky_node_ids = [lucky_proposer_id] + self.validator_node_ids
187204

188-
self.start_node(lucky_proposer_id)
189-
self.start_nodes(validators)
205+
# We'll start nodes isolated from the experiment's network, and reload
206+
# their wallets later once the experiment starts after the workaround.
207+
if not path_exists(self.tmp_dir + '/workaround'):
208+
mkdir(self.tmp_dir + '/workaround')
209+
for node_id in lucky_node_ids:
210+
initialize_datadir(self.tmp_dir + '/workaround', node_id)
211+
212+
workaround_nodes = self.build_nodes_instances(
213+
base_dir=self.tmp_dir + '/workaround',
214+
node_ids=lucky_node_ids
215+
)
216+
self.start_nodes(workaround_nodes)
190217

191218
# Although we don't need to collect data during this initialization
192219
# phase, we'll connect the nodes through a NodesHub instance to ensure
@@ -195,47 +222,62 @@ def autofinalization_workaround(self):
195222
tmp_hub = NodesHub(
196223
loop=self.loop,
197224
latency_policy=StaticLatencyPolicy(0),
198-
nodes=self.nodes,
225+
nodes=workaround_nodes,
199226
network_stats_collector=NullNetworkStatsCollector()
200227
)
201-
lucky_node_ids = [lucky_proposer_id] + self.validator_node_ids
202228
tmp_hub.sync_start_proxies(lucky_node_ids)
203229
dense_graph = create_simple_dense_graph(node_ids=lucky_node_ids)
204230
tmp_hub.sync_connect_nodes_graph(dense_graph)
205231

206232
# We have to load some money into the nodes
207-
lucky_proposer = self.nodes[lucky_proposer_id]
233+
lucky_proposer = workaround_nodes[lucky_proposer_id]
208234
for proposer_id in self.proposer_node_ids:
209235
lucky_proposer.createwallet(f'n{proposer_id}')
210236
tmp_wallet = lucky_proposer.get_wallet_rpc(f'n{proposer_id}')
211237
tmp_wallet.importmasterkey(
212238
regtest_mnemonics[proposer_id]['mnemonics']
213239
)
214240
for validator_id in self.validator_node_ids:
215-
self.nodes[validator_id].createwallet(f'n{validator_id}')
216-
tmp_wallet = self.nodes[validator_id].get_wallet_rpc(f'n{validator_id}')
241+
workaround_nodes[validator_id].createwallet(f'n{validator_id}')
242+
tmp_wallet = workaround_nodes[validator_id].get_wallet_rpc(f'n{validator_id}')
217243
tmp_wallet.importmasterkey(
218244
regtest_mnemonics[validator_id]['mnemonics']
219245
)
220246

221-
self.loop.run_until_complete(self.ensure_autofinalization_is_off())
247+
self.logger.info('Imported mnemonics into workaround nodes')
222248

223-
# Unloading the wallets that don't belong to the lucky proposer
249+
self.loop.run_until_complete(self.ensure_autofinalization_is_off(
250+
workaround_nodes
251+
))
252+
253+
# Dumping wallets to be loaded later
224254
for proposer_id in self.proposer_node_ids:
225255
tmp_wallet = lucky_proposer.get_wallet_rpc(f'n{proposer_id}')
226256
tmp_wallet.dumpwallet(normpath(self.tmp_dir + f'/n{proposer_id}.wallet'))
227-
if proposer_id != lucky_proposer_id:
228-
lucky_proposer.unloadwallet(f'n{proposer_id}')
257+
lucky_proposer.unloadwallet(f'n{proposer_id}')
258+
for validator_id in self.validator_node_ids:
259+
tmp_wallet = workaround_nodes[validator_id].get_wallet_rpc(f'n{validator_id}')
260+
tmp_wallet.dumpwallet(normpath(self.tmp_dir + f'/n{validator_id}.wallet'))
261+
262+
self.logger.info('Dumped workaround wallets to be reused later')
263+
264+
# We close all temporary connections & shut down nodes
265+
tmp_hub.close()
266+
self.stop_nodes(workaround_nodes)
229267

230-
tmp_hub.close() # We close all temporary connections
268+
# Cleaning workaround stuff
269+
rmtree(self.tmp_dir + '/workaround')
231270

232271
# We recover the original topology for the full network
233272
# self.num_nodes, self.graph_edges = tmp_num_nodes, tmp_graph_edges
234273
self.logger.info('Finished auto-finalization workaround')
235274

236-
async def ensure_autofinalization_is_off(self):
275+
async def ensure_autofinalization_is_off(
276+
self,
277+
workaround_nodes: Dict[int, TestNode]
278+
):
237279
for validator_id in self.validator_node_ids:
238-
validator = self.nodes[validator_id]
280+
validator = workaround_nodes[validator_id]
239281
tmp_wallet = validator.get_wallet_rpc(f'n{validator_id}')
240282
tmp_wallet.deposit(
241283
tmp_wallet.getnewaddress('', 'legacy'),
@@ -249,7 +291,7 @@ async def ensure_autofinalization_is_off(self):
249291
# We have to wait at least for one epoch :( .
250292
await asyncio_sleep(1 + self.block_time_seconds * 50)
251293

252-
lucky_proposer = self.nodes[self.proposer_node_ids[0]]
294+
lucky_proposer = workaround_nodes[self.proposer_node_ids[0]]
253295
is_autofinalization_off = False
254296

255297
while not is_autofinalization_off:
@@ -265,6 +307,8 @@ def safe_run(self, close_loop=True) -> bool:
265307
successful_run = False
266308
try:
267309
successful_run = self.run()
310+
except BaseException as e:
311+
self.logger.critical('The sub-experiment failed', exc_info=e)
268312
finally:
269313
self.logger.info('Releasing resources')
270314
if self.nodes_hub is not None:
@@ -299,29 +343,13 @@ def cleanup_directories(self):
299343
if self.tmp_dir != '' and path_exists(self.tmp_dir):
300344
self.logger.info('Cleaning temporary directories')
301345
rmtree(self.tmp_dir)
302-
# TODO: Remove wallet.* files too
303346

304347
def setup_chain(self):
305348
self.logger.info('Preparing "empty" chain')
306349
for i in range(self.num_nodes):
307350
initialize_datadir(self.tmp_dir, i)
308351

309-
def setup_nodes(self):
310-
if len(self.nodes) > 0:
311-
self.logger.info('Skipping nodes setup')
312-
return
313-
314-
self.logger.info('Creating node wrappers')
315-
316-
all_node_ids = set(range(self.num_nodes))
317-
self.proposer_node_ids = sample(
318-
all_node_ids, self.num_proposer_nodes
319-
)
320-
self.validator_node_ids = sample(
321-
all_node_ids.difference(self.proposer_node_ids),
322-
self.num_validator_nodes
323-
)
324-
352+
def get_node_args(self, node_id: int) -> List[str]:
325353
# Some values are copied from test_framework.util.initialize_datadir, so
326354
# they are redundant, but it's easier to see what's going on by having
327355
# all of them together.
@@ -353,36 +381,61 @@ def setup_nodes(self):
353381
for mnemonic in regtest_mnemonics
354382
]
355383
}
356-
}, separators=(",",":"))}'''
384+
}, separators=(",", ":"))}'''
357385
]
358386
relay_args = ['-proposing=0'] + node_args
359387
proposer_args = ['-proposing=1'] + node_args
360388
validator_args = ['-proposing=0', '-validating=1'] + node_args
361389

390+
if node_id in self.proposer_node_ids:
391+
_node_args = proposer_args
392+
elif node_id in self.validator_node_ids:
393+
_node_args = validator_args
394+
else:
395+
_node_args = relay_args
396+
return [
397+
f'-bind=127.0.0.1:{NodesHub.get_p2p_node_port(node_id)}',
398+
f'-rpcbind=127.0.0.1:{NodesHub.get_rpc_node_port(node_id)}',
399+
f'''-stats-log-output-file={
400+
self.nodes_stats_directory.joinpath(f"stats_{node_id}.csv")
401+
}''',
402+
f'-uacomment=simpatch{node_id}'
403+
] + _node_args
404+
405+
def setup_nodes(self):
406+
if len(self.nodes) > 0:
407+
self.logger.info('Skipping nodes setup')
408+
return
409+
410+
self.logger.info('Creating node wrappers')
411+
412+
all_node_ids = set(range(self.num_nodes))
413+
self.proposer_node_ids = sample(
414+
all_node_ids, self.num_proposer_nodes
415+
)
416+
self.validator_node_ids = sample(
417+
all_node_ids.difference(self.proposer_node_ids),
418+
self.num_validator_nodes
419+
)
420+
362421
if not self.nodes_stats_directory.exists():
363422
self.nodes_stats_directory.mkdir()
364423

365-
def get_node_args(node_id: int) -> List[str]:
366-
if node_id in self.proposer_node_ids:
367-
_node_args = proposer_args
368-
elif node_id in self.validator_node_ids:
369-
_node_args = validator_args
370-
else:
371-
_node_args = relay_args
372-
return [
373-
f'-bind=127.0.0.1:{NodesHub.get_p2p_node_port(node_id)}',
374-
f'-rpcbind=127.0.0.1:{NodesHub.get_rpc_node_port(node_id)}',
375-
f'''-stats-log-output-file={
376-
self.nodes_stats_directory.joinpath(f"stats_{node_id}.csv")
377-
}''',
378-
f'-uacomment=simpatch{node_id}'
379-
] + _node_args
380-
381-
self.nodes = [
382-
TestNode(
424+
self.nodes = self.build_nodes_instances(
425+
base_dir=self.tmp_dir,
426+
node_ids=range(self.num_nodes)
427+
)
428+
429+
def build_nodes_instances(
430+
self,
431+
base_dir: str,
432+
node_ids: Iterable[int]
433+
) -> Dict[int, TestNode]:
434+
return {
435+
i: TestNode(
383436
i=i,
384-
datadir=f'{self.tmp_dir}/node{i}',
385-
extra_args=get_node_args(i),
437+
datadir=f'{base_dir}/node{i}',
438+
extra_args=self.get_node_args(i),
386439
rpchost=None,
387440
timewait=60,
388441
unit_e=environ['UNIT_E'],
@@ -391,8 +444,8 @@ def get_node_args(node_id: int) -> List[str]:
391444
coverage_dir=None,
392445
use_cli=False
393446
)
394-
for i in range(self.num_nodes)
395-
]
447+
for i in node_ids
448+
}
396449

397450
def start_node(self, i: int):
398451
node = self.nodes[i]
@@ -403,20 +456,20 @@ def start_node(self, i: int):
403456
self.stop_nodes()
404457
raise
405458

406-
def start_nodes(self, nodes: Optional[List[TestNode]] = None):
459+
def start_nodes(self, nodes: Optional[Dict[int, TestNode]] = None):
407460
self.logger.info('Starting nodes')
408461

409462
if nodes is None:
410463
nodes = self.nodes
411464

412-
for node_id, node in enumerate(nodes):
465+
for node_id, node in nodes.items():
413466
try:
414467
if not node.running:
415468
node.start()
416469
except OSError as e:
417470
self.logger.critical(f'Node {node_id} failed to start', e)
418471
raise
419-
for node_id, node in enumerate(nodes):
472+
for node_id, node in nodes.items():
420473
try:
421474
node.wait_for_rpc_connection()
422475
except AssertionError as e:
@@ -428,14 +481,18 @@ def start_nodes(self, nodes: Optional[List[TestNode]] = None):
428481

429482
self.logger.info('Started nodes')
430483

431-
def stop_nodes(self):
484+
def stop_nodes(self, nodes: Optional[Dict[int, TestNode]] = None):
432485
self.logger.info('Stopping nodes')
433-
for node in self.nodes:
486+
487+
if nodes is None:
488+
nodes = self.nodes
489+
490+
for node in nodes.values():
434491
try:
435492
node.stop_node()
436493
except AssertionError:
437494
continue
438-
for node in self.nodes:
495+
for node in nodes.values():
439496
node.wait_until_stopped()
440497

441498
def define_network_topology(self):

0 commit comments

Comments
 (0)