@@ -3819,6 +3819,251 @@ mod tests {
38193819 no_recertification_after_replay :: < _ , _ , RoundRobin > ( secp256r1:: fixture) ;
38203820 }
38213821
3822+ /// After restart, a recovered notarization for a leader-owned round should still
3823+ /// skip application certification.
3824+ fn local_recovered_notarization_skips_certify_after_restart < S , F > ( mut fixture : F )
3825+ where
3826+ S : Scheme < Sha256Digest , PublicKey = PublicKey > ,
3827+ F : FnMut ( & mut deterministic:: Context , & [ u8 ] , u32 ) -> Fixture < S > ,
3828+ {
3829+ let n = 5 ;
3830+ let quorum = quorum ( n) ;
3831+ let namespace = b"local_recovered_notarization_skips_certify_after_restart" . to_vec ( ) ;
3832+ let executor = deterministic:: Runner :: timed ( Duration :: from_secs ( 20 ) ) ;
3833+ executor. start ( |mut context| async move {
3834+ let ( network, oracle) = Network :: new (
3835+ context. with_label ( "network" ) ,
3836+ NConfig {
3837+ max_size : 1024 * 1024 ,
3838+ disconnect_on_block : true ,
3839+ tracked_peer_sets : None ,
3840+ } ,
3841+ ) ;
3842+ network. start ( ) ;
3843+
3844+ let Fixture {
3845+ participants,
3846+ schemes,
3847+ ..
3848+ } = fixture ( & mut context, & namespace, n) ;
3849+
3850+ let me = participants[ 0 ] . clone ( ) ;
3851+ let elector = RoundRobin :: < Sha256 > :: default ( ) ;
3852+ let built_elector: RoundRobinElector < S > = elector
3853+ . clone ( )
3854+ . build ( & participants. clone ( ) . try_into ( ) . unwrap ( ) ) ;
3855+ let reporter_cfg = mocks:: reporter:: Config {
3856+ participants : participants. clone ( ) . try_into ( ) . unwrap ( ) ,
3857+ scheme : schemes[ 0 ] . clone ( ) ,
3858+ elector : elector. clone ( ) ,
3859+ } ;
3860+ let reporter =
3861+ mocks:: reporter:: Reporter :: new ( context. with_label ( "reporter" ) , reporter_cfg) ;
3862+ let relay = Arc :: new ( mocks:: relay:: Relay :: new ( ) ) ;
3863+ let partition = "local_recovered_notarization_skips_certify_after_restart" . to_string ( ) ;
3864+ let epoch = Epoch :: new ( 333 ) ;
3865+
3866+ let app_cfg = mocks:: application:: Config {
3867+ hasher : Sha256 :: default ( ) ,
3868+ relay : relay. clone ( ) ,
3869+ me : me. clone ( ) ,
3870+ propose_latency : ( 1.0 , 0.0 ) ,
3871+ verify_latency : ( 1.0 , 0.0 ) ,
3872+ certify_latency : ( 1.0 , 0.0 ) ,
3873+ should_certify : mocks:: application:: Certifier :: Always ,
3874+ } ;
3875+ let ( app_actor, application) =
3876+ mocks:: application:: Application :: new ( context. with_label ( "app_initial" ) , app_cfg) ;
3877+ app_actor. start ( ) ;
3878+
3879+ let voter_cfg = Config {
3880+ scheme : schemes[ 0 ] . clone ( ) ,
3881+ elector : elector. clone ( ) ,
3882+ blocker : oracle. control ( me. clone ( ) ) ,
3883+ automaton : application. clone ( ) ,
3884+ relay : application. clone ( ) ,
3885+ reporter : reporter. clone ( ) ,
3886+ partition : partition. clone ( ) ,
3887+ epoch,
3888+ mailbox_size : 128 ,
3889+ leader_timeout : Duration :: from_secs ( 5 ) ,
3890+ certification_timeout : Duration :: from_secs ( 5 ) ,
3891+ timeout_retry : Duration :: from_mins ( 60 ) ,
3892+ activity_timeout : ViewDelta :: new ( 10 ) ,
3893+ replay_buffer : NZUsize ! ( 1024 * 1024 ) ,
3894+ write_buffer : NZUsize ! ( 1024 * 1024 ) ,
3895+ page_cache : CacheRef :: from_pooler ( & context, PAGE_SIZE , PAGE_CACHE_SIZE ) ,
3896+ } ;
3897+ let ( voter, mut mailbox) = Actor :: new ( context. with_label ( "voter_initial" ) , voter_cfg) ;
3898+
3899+ let ( resolver_sender, _resolver_receiver) = mpsc:: channel ( 8 ) ;
3900+ let ( batcher_sender, mut batcher_receiver) = mpsc:: channel ( 8 ) ;
3901+ let ( vote_sender, _) = oracle
3902+ . control ( me. clone ( ) )
3903+ . register ( 0 , TEST_QUOTA )
3904+ . await
3905+ . unwrap ( ) ;
3906+ let ( cert_sender, _) = oracle
3907+ . control ( me. clone ( ) )
3908+ . register ( 1 , TEST_QUOTA )
3909+ . await
3910+ . unwrap ( ) ;
3911+
3912+ let handle = voter. start (
3913+ batcher:: Mailbox :: new ( batcher_sender) ,
3914+ resolver:: Mailbox :: new ( resolver_sender) ,
3915+ vote_sender,
3916+ cert_sender,
3917+ ) ;
3918+
3919+ if let batcher:: Message :: Update { response, .. } =
3920+ batcher_receiver. recv ( ) . await . unwrap ( )
3921+ {
3922+ response. send ( None ) . unwrap ( ) ;
3923+ }
3924+
3925+ let target_view = View :: new ( 2 ) ;
3926+ advance_to_view (
3927+ & mut mailbox,
3928+ & mut batcher_receiver,
3929+ & schemes,
3930+ quorum,
3931+ target_view,
3932+ )
3933+ . await ;
3934+ assert_eq ! (
3935+ built_elector. elect( Round :: new( epoch, target_view) , None ) ,
3936+ Participant :: new( 0 ) ,
3937+ "we should be leader at view 2"
3938+ ) ;
3939+
3940+ handle. abort ( ) ;
3941+
3942+ let certify_calls: Arc < Mutex < Vec < Sha256Digest > > > = Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ;
3943+ let tracker = certify_calls. clone ( ) ;
3944+ let app_cfg = mocks:: application:: Config {
3945+ hasher : Sha256 :: default ( ) ,
3946+ relay : relay. clone ( ) ,
3947+ me : me. clone ( ) ,
3948+ propose_latency : ( 1.0 , 0.0 ) ,
3949+ verify_latency : ( 1.0 , 0.0 ) ,
3950+ certify_latency : ( 1.0 , 0.0 ) ,
3951+ should_certify : mocks:: application:: Certifier :: Custom ( Box :: new ( move |digest| {
3952+ tracker. lock ( ) . push ( digest) ;
3953+ false
3954+ } ) ) ,
3955+ } ;
3956+ let ( app_actor, application) =
3957+ mocks:: application:: Application :: new ( context. with_label ( "app_restarted" ) , app_cfg) ;
3958+ app_actor. start ( ) ;
3959+
3960+ let voter_cfg = Config {
3961+ scheme : schemes[ 0 ] . clone ( ) ,
3962+ elector,
3963+ blocker : oracle. control ( me. clone ( ) ) ,
3964+ automaton : application. clone ( ) ,
3965+ relay : application. clone ( ) ,
3966+ reporter,
3967+ partition,
3968+ epoch,
3969+ mailbox_size : 128 ,
3970+ leader_timeout : Duration :: from_secs ( 100 ) ,
3971+ certification_timeout : Duration :: from_secs ( 100 ) ,
3972+ timeout_retry : Duration :: from_mins ( 60 ) ,
3973+ activity_timeout : ViewDelta :: new ( 10 ) ,
3974+ replay_buffer : NZUsize ! ( 1024 * 1024 ) ,
3975+ write_buffer : NZUsize ! ( 1024 * 1024 ) ,
3976+ page_cache : CacheRef :: from_pooler ( & context, PAGE_SIZE , PAGE_CACHE_SIZE ) ,
3977+ } ;
3978+ let ( voter, mut mailbox) = Actor :: new ( context. with_label ( "voter_restarted" ) , voter_cfg) ;
3979+
3980+ let ( resolver_sender, _resolver_receiver) = mpsc:: channel ( 8 ) ;
3981+ let ( batcher_sender, mut batcher_receiver) = mpsc:: channel ( 8 ) ;
3982+ let ( vote_sender, _) = oracle
3983+ . control ( me. clone ( ) )
3984+ . register ( 2 , TEST_QUOTA )
3985+ . await
3986+ . unwrap ( ) ;
3987+ let ( cert_sender, _) = oracle
3988+ . control ( me. clone ( ) )
3989+ . register ( 3 , TEST_QUOTA )
3990+ . await
3991+ . unwrap ( ) ;
3992+
3993+ voter. start (
3994+ batcher:: Mailbox :: new ( batcher_sender) ,
3995+ resolver:: Mailbox :: new ( resolver_sender) ,
3996+ vote_sender,
3997+ cert_sender,
3998+ ) ;
3999+
4000+ if let batcher:: Message :: Update { response, .. } =
4001+ batcher_receiver. recv ( ) . await . unwrap ( )
4002+ {
4003+ response. send ( None ) . unwrap ( ) ;
4004+ }
4005+
4006+ let proposal = Proposal :: new (
4007+ Round :: new ( epoch, target_view) ,
4008+ target_view. previous ( ) . unwrap ( ) ,
4009+ Sha256 :: hash ( b"recovered_local_leader_proposal" ) ,
4010+ ) ;
4011+ let ( _, notarization) = build_notarization ( & schemes, & proposal, quorum) ;
4012+ mailbox
4013+ . recovered ( Certificate :: Notarization ( notarization) )
4014+ . await ;
4015+
4016+ loop {
4017+ select ! {
4018+ msg = batcher_receiver. recv( ) => match msg. unwrap( ) {
4019+ batcher:: Message :: Constructed ( Vote :: Finalize ( finalize) )
4020+ if finalize. view( ) == target_view =>
4021+ {
4022+ assert_eq!( finalize. proposal, proposal) ;
4023+ break ;
4024+ }
4025+ batcher:: Message :: Constructed ( Vote :: Nullify ( nullify) )
4026+ if nullify. view( ) == target_view =>
4027+ {
4028+ panic!(
4029+ "leader-owned recovered proposal should finalize instead of nullifying view {target_view}"
4030+ ) ;
4031+ }
4032+ batcher:: Message :: Update { response, .. } => response. send( None ) . unwrap( ) ,
4033+ _ => { }
4034+ } ,
4035+ _ = context. sleep( Duration :: from_secs( 5 ) ) => {
4036+ panic!( "expected finalize for recovered leader proposal at view {target_view}" ) ;
4037+ } ,
4038+ }
4039+ }
4040+
4041+ assert_eq ! (
4042+ certify_calls. lock( ) . len( ) ,
4043+ 0 ,
4044+ "application certify should not run for a recovered leader proposal" ,
4045+ ) ;
4046+ } ) ;
4047+ }
4048+
4049+ #[ test_traced]
4050+ fn test_local_recovered_notarization_skips_certify_after_restart ( ) {
4051+ local_recovered_notarization_skips_certify_after_restart :: < _ , _ > (
4052+ bls12381_threshold_vrf:: fixture :: < MinPk , _ > ,
4053+ ) ;
4054+ local_recovered_notarization_skips_certify_after_restart :: < _ , _ > (
4055+ bls12381_threshold_vrf:: fixture :: < MinSig , _ > ,
4056+ ) ;
4057+ local_recovered_notarization_skips_certify_after_restart :: < _ , _ > (
4058+ bls12381_multisig:: fixture :: < MinPk , _ > ,
4059+ ) ;
4060+ local_recovered_notarization_skips_certify_after_restart :: < _ , _ > (
4061+ bls12381_multisig:: fixture :: < MinSig , _ > ,
4062+ ) ;
4063+ local_recovered_notarization_skips_certify_after_restart :: < _ , _ > ( ed25519:: fixture) ;
4064+ local_recovered_notarization_skips_certify_after_restart :: < _ , _ > ( secp256r1:: fixture) ;
4065+ }
4066+
38224067 /// Test that in-flight certification requests are cancelled when finalization occurs.
38234068 ///
38244069 /// 1. Use a very long certify latency to ensure certification is in-flight.
0 commit comments