Skip to content

Commit 2854b03

Browse files
committed
cover restart recovery case
1 parent 77e59bf commit 2854b03

3 files changed

Lines changed: 291 additions & 2 deletions

File tree

consensus/src/simplex/actors/voter/mod.rs

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.

consensus/src/simplex/actors/voter/round.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,14 +360,23 @@ impl<S: Scheme, D: Digest> Round<S, D> {
360360
///
361361
/// Returns the leader's public key if equivocation is detected (conflicting proposals).
362362
pub fn add_recovered_proposal(&mut self, proposal: Proposal<D>) -> Option<S::PublicKey> {
363+
let proposed_locally = self
364+
.leader()
365+
.as_ref()
366+
.is_some_and(|leader| self.is_signer(leader.idx));
363367
match self.proposal.update(&proposal, true) {
364368
ProposalChange::New => {
369+
self.proposal.set_proposed_locally(proposed_locally);
365370
debug!(?proposal, "setting proposal from certificate");
366371
self.leader_deadline = None;
367372
None
368373
}
369-
ProposalChange::Unchanged => None,
374+
ProposalChange::Unchanged => {
375+
self.proposal.set_proposed_locally(proposed_locally);
376+
None
377+
}
370378
ProposalChange::Equivocated { dropped, retained } => {
379+
self.proposal.set_proposed_locally(proposed_locally);
371380
// Receiving a certificate for a conflicting proposal means the
372381
// leader signed two different payloads for the same (epoch,
373382
// view).
@@ -1071,7 +1080,7 @@ mod tests {
10711080
let proposal = Proposal::new(round_info, View::new(0), Sha256Digest::from([1u8; 32]));
10721081

10731082
let mut round = Round::new(local_scheme, round_info, now);
1074-
round.set_leader(Participant::new(0));
1083+
round.set_leader(Participant::new(1));
10751084
// Don't set proposal yet
10761085

10771086
// Add notarization (which includes the proposal in the certificate)
@@ -1091,6 +1100,36 @@ mod tests {
10911100
assert!(!round.proposed_locally());
10921101
}
10931102

1103+
#[test]
1104+
fn recovered_local_leader_proposal_stays_local() {
1105+
let mut rng = test_rng();
1106+
let namespace = b"ns";
1107+
let Fixture {
1108+
schemes, verifier, ..
1109+
} = ed25519::fixture(&mut rng, namespace, 4);
1110+
let local_scheme = schemes[0].clone();
1111+
1112+
let now = SystemTime::UNIX_EPOCH;
1113+
let round_info = Rnd::new(Epoch::new(1), View::new(1));
1114+
let proposal = Proposal::new(round_info, View::new(0), Sha256Digest::from([8u8; 32]));
1115+
1116+
let mut round = Round::new(local_scheme, round_info, now);
1117+
round.set_leader(Participant::new(0));
1118+
1119+
let notarization_votes: Vec<_> = schemes
1120+
.iter()
1121+
.map(|scheme| Notarize::sign(scheme, proposal.clone()).unwrap())
1122+
.collect();
1123+
let notarization =
1124+
Notarization::from_notarizes(&verifier, notarization_votes.iter(), &Sequential)
1125+
.unwrap();
1126+
let (added, _) = round.add_notarization(notarization);
1127+
assert!(added);
1128+
1129+
assert!(round.proposed_locally());
1130+
assert!(round.try_certify().is_some());
1131+
}
1132+
10941133
#[test]
10951134
fn locally_built_proposals_stay_marked_local_for_certification() {
10961135
let mut rng = test_rng();

consensus/src/simplex/actors/voter/slot.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ where
6767
self.proposed_locally
6868
}
6969

70+
/// Updates whether the tracked proposal belongs to the local participant.
71+
pub const fn set_proposed_locally(&mut self, proposed_locally: bool) {
72+
self.proposed_locally = proposed_locally;
73+
}
74+
7075
/// Returns whether the slot contains a concrete proposal and no equivocation.
7176
pub fn has_unequivocated_proposal(&self) -> bool {
7277
self.proposal.is_some() && self.status != Status::Equivocated

0 commit comments

Comments
 (0)