@@ -73,7 +73,6 @@ mod test {
7373 #[ case:: fail_on_prevote_rx( Some ( InjectFailureConfig { height: 4 , trigger: InjectFailureTrigger :: PrevoteRx } ) ) ]
7474 #[ case:: fail_on_precommit_rx( Some ( InjectFailureConfig { height: 4 , trigger: InjectFailureTrigger :: PrecommitRx } ) ) ]
7575 #[ case:: fail_on_proposal_decided( Some ( InjectFailureConfig { height: 4 , trigger: InjectFailureTrigger :: ProposalDecided } ) ) ]
76- #[ case:: fail_on_proposal_committed( Some ( InjectFailureConfig { height: 4 , trigger: InjectFailureTrigger :: ProposalCommitted } ) ) ]
7776 #[ tokio:: test]
7877 async fn consensus_3_nodes_with_failures ( #[ case] inject_failure : Option < InjectFailureConfig > ) {
7978 const NUM_NODES : usize = 3 ;
@@ -233,6 +232,170 @@ mod test {
233232 ) ;
234233 }
235234
235+ #[ rstest]
236+ // Cannot be tested with just 3 nodes b/c Bob might break the
237+ // Gossip communication property when restarting - see
238+ // https://github.com/eqlabs/pathfinder/issues/3286
239+ #[ case:: fail_on_proposal_committed( InjectFailureConfig { height: 4 , trigger: InjectFailureTrigger :: ProposalCommitted } ) ]
240+ #[ tokio:: test]
241+ async fn consensus_4_nodes_with_failures ( #[ case] inject_failure : InjectFailureConfig ) {
242+ const NUM_NODES : usize = 4 ;
243+ const READY_TIMEOUT : Duration = Duration :: from_secs ( 20 ) ;
244+ const TEST_TIMEOUT : Duration = Duration :: from_secs ( 120 ) ;
245+ const POLL_READY : Duration = Duration :: from_millis ( 500 ) ;
246+ const POLL_HEIGHT : Duration = Duration :: from_secs ( 1 ) ;
247+
248+ let disallow_reverted_txns = true ;
249+
250+ let ( configs, boot_height, stopwatch) =
251+ utils:: setup ( NUM_NODES , disallow_reverted_txns) . unwrap ( ) ;
252+
253+ let target_height: u64 = boot_height + 5 ;
254+
255+ let alice_cfg = configs. first ( ) . unwrap ( ) ;
256+ let mut fgw = FeederGateway :: spawn ( alice_cfg) . unwrap ( ) ;
257+ fgw. wait_for_ready ( POLL_READY , READY_TIMEOUT ) . await . unwrap ( ) ;
258+
259+ let mut configs = configs. into_iter ( ) . map ( |cfg| {
260+ cfg. with_local_feeder_gateway ( fgw. port ( ) )
261+ . with_sync_enabled ( )
262+ } ) ;
263+
264+ let alice = PathfinderInstance :: spawn ( configs. next ( ) . unwrap ( ) ) . unwrap ( ) ;
265+ alice
266+ . wait_for_ready ( POLL_READY , READY_TIMEOUT )
267+ . await
268+ . unwrap ( ) ;
269+
270+ let boot_port = alice. consensus_p2p_port ( ) ;
271+ let mut configs = configs. map ( |cfg| cfg. with_boot_port ( boot_port) ) ;
272+
273+ let bob_cfg = configs
274+ . next ( )
275+ . unwrap ( )
276+ . with_inject_failure ( Some ( inject_failure) ) ;
277+
278+ let bob = PathfinderInstance :: spawn ( bob_cfg. clone ( ) ) . unwrap ( ) ;
279+ let charlie = PathfinderInstance :: spawn ( configs. next ( ) . unwrap ( ) ) . unwrap ( ) ;
280+ let dan = PathfinderInstance :: spawn ( configs. next ( ) . unwrap ( ) ) . unwrap ( ) ;
281+
282+ let ( bob_rdy, charlie_rdy, dan_rdy) = tokio:: join!(
283+ bob. wait_for_ready( POLL_READY , READY_TIMEOUT ) ,
284+ charlie. wait_for_ready( POLL_READY , READY_TIMEOUT ) ,
285+ dan. wait_for_ready( POLL_READY , READY_TIMEOUT ) ,
286+ ) ;
287+ bob_rdy. unwrap ( ) ;
288+ charlie_rdy. unwrap ( ) ;
289+ dan_rdy. unwrap ( ) ;
290+
291+ utils:: log_elapsed ( stopwatch) ;
292+
293+ let ( hnr_tx, hnr_rx) = mpsc:: channel ( target_height as usize * 3 ) ;
294+ let hnr_rx = tokio_stream:: wrappers:: ReceiverStream :: new ( hnr_rx) ;
295+ let ( err_tx, err_rx) = mpsc:: channel ( 6 ) ;
296+
297+ let alice_decided = wait_for_height (
298+ & alice,
299+ target_height,
300+ POLL_HEIGHT ,
301+ Some ( hnr_tx) ,
302+ err_tx. clone ( ) ,
303+ ) ;
304+ let bob_decided = wait_for_height ( & bob, target_height, POLL_HEIGHT , None , err_tx. clone ( ) ) ;
305+ let charlie_decided =
306+ wait_for_height ( & charlie, target_height, POLL_HEIGHT , None , err_tx. clone ( ) ) ;
307+ let dan_decided = wait_for_height ( & dan, target_height, POLL_HEIGHT , None , err_tx. clone ( ) ) ;
308+ let alice_committed = wait_for_block_exists (
309+ & alice,
310+ target_height,
311+ POLL_HEIGHT ,
312+ disallow_reverted_txns,
313+ err_tx. clone ( ) ,
314+ ) ;
315+ let bob_committed = wait_for_block_exists (
316+ & bob,
317+ target_height,
318+ POLL_HEIGHT ,
319+ disallow_reverted_txns,
320+ err_tx. clone ( ) ,
321+ ) ;
322+ let charlie_committed = wait_for_block_exists (
323+ & charlie,
324+ target_height,
325+ POLL_HEIGHT ,
326+ disallow_reverted_txns,
327+ err_tx. clone ( ) ,
328+ ) ;
329+ let dan_committed = wait_for_block_exists (
330+ & charlie,
331+ target_height,
332+ POLL_HEIGHT ,
333+ disallow_reverted_txns,
334+ err_tx. clone ( ) ,
335+ ) ;
336+
337+ let maybe_bob = respawn_on_fail ( true , bob, bob_cfg, POLL_READY , READY_TIMEOUT ) ;
338+
339+ // Wait for: the test to pass, timeout, user interruption, or bail out early if
340+ // the RPC client encounters an error
341+ utils:: join_all (
342+ vec ! [
343+ alice_decided,
344+ bob_decided,
345+ charlie_decided,
346+ dan_decided,
347+ alice_committed,
348+ bob_committed,
349+ charlie_committed,
350+ dan_committed,
351+ ] ,
352+ TEST_TIMEOUT ,
353+ err_rx,
354+ )
355+ . await
356+ . unwrap ( ) ;
357+
358+ let decided_hnrs = hnr_rx. collect :: < Vec < _ > > ( ) . await ;
359+ if let Some ( x) = decided_hnrs. iter ( ) . find ( |hnr| hnr. round ( ) > 0 ) {
360+ println ! ( "Network failed to recover in round 0 at (h:r): {x}" ) ;
361+ }
362+
363+ let alice_artifacts = get_cached_artifacts_info ( & alice, target_height)
364+ . await
365+ . unwrap ( ) ;
366+ assert ! (
367+ alice_artifacts. is_empty( ) ,
368+ "Alice should not have leftover cached consensus data: {alice_artifacts:#?}"
369+ ) ;
370+
371+ if let Some ( bob) = maybe_bob. instance ( ) {
372+ let bob_artifacts = get_cached_artifacts_info ( & bob, target_height)
373+ . await
374+ . unwrap ( ) ;
375+ assert ! (
376+ bob_artifacts. is_empty( ) ,
377+ "Bob should not have leftover cached consensus data after respawn: \
378+ {bob_artifacts:#?}"
379+ ) ;
380+ }
381+
382+ let charlie_artifacts = get_cached_artifacts_info ( & charlie, target_height)
383+ . await
384+ . unwrap ( ) ;
385+ assert ! (
386+ charlie_artifacts. is_empty( ) ,
387+ "Charlie should not have leftover cached consensus data: {charlie_artifacts:#?}"
388+ ) ;
389+
390+ let dan_artifacts = get_cached_artifacts_info ( & dan, target_height)
391+ . await
392+ . unwrap ( ) ;
393+ assert ! (
394+ dan_artifacts. is_empty( ) ,
395+ "Dan should not have leftover cached consensus data: {dan_artifacts:#?}"
396+ ) ;
397+ }
398+
236399 #[ tokio:: test]
237400 async fn consensus_3_nodes_fourth_node_joins_late_can_catch_up ( ) {
238401 const NUM_NODES : usize = 4 ;
0 commit comments