Skip to content

Commit 3100e6a

Browse files
apollo_consensus,apollo_protobuf: use BlockNumber for Vote.height
1 parent 0045f6c commit 3100e6a

File tree

9 files changed

+154
-108
lines changed

9 files changed

+154
-108
lines changed

crates/apollo_consensus/src/manager.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ impl<ContextT: ConsensusContext> ConsensusCache<ContextT> {
238238

239239
/// Caches a vote for a future height.
240240
fn cache_future_vote(&mut self, vote: Vote) {
241-
self.future_votes.entry(BlockNumber(vote.height)).or_default().push(vote);
241+
self.future_votes.entry(vote.height).or_default().push(vote);
242242
}
243243

244244
/// Caches a proposal for a future height.
@@ -666,7 +666,7 @@ impl<ContextT: ConsensusContext> MultiHeightManager<ContextT> {
666666
// TODO(matan): We need to figure out an actual caching strategy under 2 constraints:
667667
// 1. Malicious - must be capped so a malicious peer can't DoS us.
668668
// 2. Parallel proposals - we may send/receive a proposal for (H+1, 0).
669-
match message.height.cmp(&height.0) {
669+
match message.height.cmp(&height) {
670670
std::cmp::Ordering::Greater => {
671671
if self.should_cache_vote(&height, 0, &message) {
672672
trace!("Cache message for a future height. {:?}", message);
@@ -825,12 +825,12 @@ impl<ContextT: ConsensusContext> MultiHeightManager<ContextT> {
825825
&self,
826826
current_height: &BlockNumber,
827827
current_round: Round,
828-
msg_height: u64,
828+
msg_height: BlockNumber,
829829
msg_round: Round,
830830
msg_description: &str,
831831
) -> bool {
832832
let limits = &self.consensus_config.dynamic_config.future_msg_limit;
833-
let height_diff = msg_height.saturating_sub(current_height.0);
833+
let height_diff = msg_height.0.saturating_sub(current_height.0);
834834

835835
let should_cache = height_diff <= limits.future_height_limit.into()
836836
// For current height, check against current round + future_round_limit
@@ -865,7 +865,7 @@ impl<ContextT: ConsensusContext> MultiHeightManager<ContextT> {
865865
self.should_cache_msg(
866866
current_height,
867867
current_round,
868-
proposal.height.0,
868+
proposal.height,
869869
proposal.round,
870870
"proposal",
871871
)

crates/apollo_consensus/src/manager_test.rs

Lines changed: 60 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -143,16 +143,16 @@ async fn manager_multiple_heights_unordered(consensus_config: ConsensusConfig) {
143143
vec![TestProposalPart::Init(proposal_init(2, 0, *PROPOSER_ID))],
144144
)
145145
.await;
146-
send(&mut sender, prevote(Some(Felt::TWO), 2, 0, *PROPOSER_ID)).await;
147-
send(&mut sender, precommit(Some(Felt::TWO), 2, 0, *PROPOSER_ID)).await;
146+
send(&mut sender, prevote(Some(Felt::TWO), BlockNumber(2), 0, *PROPOSER_ID)).await;
147+
send(&mut sender, precommit(Some(Felt::TWO), BlockNumber(2), 0, *PROPOSER_ID)).await;
148148

149149
send_proposal(
150150
&mut proposal_receiver_sender,
151151
vec![TestProposalPart::Init(proposal_init(1, 0, *PROPOSER_ID))],
152152
)
153153
.await;
154-
send(&mut sender, prevote(Some(Felt::ONE), 1, 0, *PROPOSER_ID)).await;
155-
send(&mut sender, precommit(Some(Felt::ONE), 1, 0, *PROPOSER_ID)).await;
154+
send(&mut sender, prevote(Some(Felt::ONE), BlockNumber(1), 0, *PROPOSER_ID)).await;
155+
send(&mut sender, precommit(Some(Felt::ONE), BlockNumber(1), 0, *PROPOSER_ID)).await;
156156

157157
let mut context = MockTestContext::new();
158158
// Run the manager for height 1.
@@ -210,7 +210,9 @@ async fn run_consensus_sync(consensus_config: ConsensusConfig) {
210210
context.expect_broadcast().returning(move |_| Ok(()));
211211
context
212212
.expect_decision_reached()
213-
.withf(move |block, votes| *block == ProposalCommitment(Felt::TWO) && votes[0].height == 2)
213+
.withf(move |block, votes| {
214+
*block == ProposalCommitment(Felt::TWO) && votes[0].height.0 == 2
215+
})
214216
.return_once(move |_, _| {
215217
decision_tx.send(()).unwrap();
216218
Ok(())
@@ -231,8 +233,8 @@ async fn run_consensus_sync(consensus_config: ConsensusConfig) {
231233
let TestSubscriberChannels { mock_network, subscriber_channels } =
232234
mock_register_broadcast_topic().unwrap();
233235
let mut network_sender = mock_network.broadcasted_messages_sender;
234-
send(&mut network_sender, prevote(Some(Felt::TWO), 2, 0, *PROPOSER_ID)).await;
235-
send(&mut network_sender, precommit(Some(Felt::TWO), 2, 0, *PROPOSER_ID)).await;
236+
send(&mut network_sender, prevote(Some(Felt::TWO), BlockNumber(2), 0, *PROPOSER_ID)).await;
237+
send(&mut network_sender, precommit(Some(Felt::TWO), BlockNumber(2), 0, *PROPOSER_ID)).await;
236238
let run_consensus_args = RunConsensusArguments {
237239
consensus_config,
238240
start_active_height: BlockNumber(1),
@@ -270,10 +272,10 @@ async fn test_timeouts(consensus_config: ConsensusConfig) {
270272
vec![TestProposalPart::Init(proposal_init(1, 0, *PROPOSER_ID))],
271273
)
272274
.await;
273-
send(&mut sender, prevote(None, 1, 0, *VALIDATOR_ID_2)).await;
274-
send(&mut sender, prevote(None, 1, 0, *VALIDATOR_ID_3)).await;
275-
send(&mut sender, precommit(None, 1, 0, *VALIDATOR_ID_2)).await;
276-
send(&mut sender, precommit(None, 1, 0, *VALIDATOR_ID_3)).await;
275+
send(&mut sender, prevote(None, BlockNumber(1), 0, *VALIDATOR_ID_2)).await;
276+
send(&mut sender, prevote(None, BlockNumber(1), 0, *VALIDATOR_ID_3)).await;
277+
send(&mut sender, precommit(None, BlockNumber(1), 0, *VALIDATOR_ID_2)).await;
278+
send(&mut sender, precommit(None, BlockNumber(1), 0, *VALIDATOR_ID_3)).await;
277279

278280
let mut context = MockTestContext::new();
279281
context.expect_set_height_and_round().returning(move |_, _| ());
@@ -289,7 +291,7 @@ async fn test_timeouts(consensus_config: ConsensusConfig) {
289291
context
290292
.expect_broadcast()
291293
.times(1)
292-
.withf(move |msg: &Vote| msg == &prevote(None, 1, 1, *VALIDATOR_ID))
294+
.withf(move |msg: &Vote| msg == &prevote(None, BlockNumber(1), 1, *VALIDATOR_ID))
293295
.return_once(move |_| {
294296
timeout_send.send(()).unwrap();
295297
Ok(())
@@ -324,11 +326,11 @@ async fn test_timeouts(consensus_config: ConsensusConfig) {
324326
vec![TestProposalPart::Init(proposal_init(1, 1, *PROPOSER_ID))],
325327
)
326328
.await;
327-
send(&mut sender, prevote(Some(Felt::ONE), 1, 1, *PROPOSER_ID)).await;
328-
send(&mut sender, prevote(Some(Felt::ONE), 1, 1, *VALIDATOR_ID_2)).await;
329-
send(&mut sender, prevote(Some(Felt::ONE), 1, 1, *VALIDATOR_ID_3)).await;
330-
send(&mut sender, precommit(Some(Felt::ONE), 1, 1, *VALIDATOR_ID_2)).await;
331-
send(&mut sender, precommit(Some(Felt::ONE), 1, 1, *VALIDATOR_ID_3)).await;
329+
send(&mut sender, prevote(Some(Felt::ONE), BlockNumber(1), 1, *PROPOSER_ID)).await;
330+
send(&mut sender, prevote(Some(Felt::ONE), BlockNumber(1), 1, *VALIDATOR_ID_2)).await;
331+
send(&mut sender, prevote(Some(Felt::ONE), BlockNumber(1), 1, *VALIDATOR_ID_3)).await;
332+
send(&mut sender, precommit(Some(Felt::ONE), BlockNumber(1), 1, *VALIDATOR_ID_2)).await;
333+
send(&mut sender, precommit(Some(Felt::ONE), BlockNumber(1), 1, *VALIDATOR_ID_3)).await;
332334

333335
manager_handle.await.unwrap();
334336
}
@@ -353,7 +355,7 @@ async fn timely_message_handling(consensus_config: ConsensusConfig) {
353355
let mut subscriber_channels = subscriber_channels.into();
354356
let mut vote_sender = mock_network.broadcasted_messages_sender;
355357
let metadata = BroadcastedMessageMetadata::get_test_instance(&mut get_rng());
356-
let vote = prevote(Some(Felt::TWO), 1, 0, *PROPOSER_ID);
358+
let vote = prevote(Some(Felt::TWO), BlockNumber(1), 0, *PROPOSER_ID);
357359
// Fill up the buffer.
358360
while vote_sender.send((vote.clone(), metadata.clone())).now_or_never().is_some() {}
359361

@@ -401,26 +403,26 @@ async fn future_height_limit_caching_and_dropping(mut consensus_config: Consensu
401403
vec![TestProposalPart::Init(proposal_init(2, 0, *PROPOSER_ID))],
402404
)
403405
.await;
404-
send(&mut sender, prevote(Some(Felt::TWO), 2, 0, *PROPOSER_ID)).await;
405-
send(&mut sender, precommit(Some(Felt::TWO), 2, 0, *PROPOSER_ID)).await;
406+
send(&mut sender, prevote(Some(Felt::TWO), BlockNumber(2), 0, *PROPOSER_ID)).await;
407+
send(&mut sender, precommit(Some(Felt::TWO), BlockNumber(2), 0, *PROPOSER_ID)).await;
406408

407409
// Send proposal and votes for height 1 (should be cached when processing height 0).
408410
send_proposal(
409411
&mut proposal_receiver_sender,
410412
vec![TestProposalPart::Init(proposal_init(1, 0, *PROPOSER_ID))],
411413
)
412414
.await;
413-
send(&mut sender, prevote(Some(Felt::ONE), 1, 0, *PROPOSER_ID)).await;
414-
send(&mut sender, precommit(Some(Felt::ONE), 1, 0, *PROPOSER_ID)).await;
415+
send(&mut sender, prevote(Some(Felt::ONE), BlockNumber(1), 0, *PROPOSER_ID)).await;
416+
send(&mut sender, precommit(Some(Felt::ONE), BlockNumber(1), 0, *PROPOSER_ID)).await;
415417

416418
// Send proposal and votes for height 0 (current height - needed to reach consensus).
417419
send_proposal(
418420
&mut proposal_receiver_sender,
419421
vec![TestProposalPart::Init(proposal_init(0, 0, *PROPOSER_ID))],
420422
)
421423
.await;
422-
send(&mut sender, prevote(Some(Felt::ZERO), 0, 0, *PROPOSER_ID)).await;
423-
send(&mut sender, precommit(Some(Felt::ZERO), 0, 0, *PROPOSER_ID)).await;
424+
send(&mut sender, prevote(Some(Felt::ZERO), BlockNumber(0), 0, *PROPOSER_ID)).await;
425+
send(&mut sender, precommit(Some(Felt::ZERO), BlockNumber(0), 0, *PROPOSER_ID)).await;
424426

425427
let mut context = MockTestContext::new();
426428
context.expect_try_sync().returning(|_| false);
@@ -434,7 +436,9 @@ async fn future_height_limit_caching_and_dropping(mut consensus_config: Consensu
434436
let (height2_nil_vote_trigger, height2_nil_vote_wait) = oneshot::channel();
435437
context
436438
.expect_broadcast()
437-
.withf(move |vote: &Vote| vote.height == 2 && vote.proposal_commitment.is_none())
439+
.withf(move |vote: &Vote| {
440+
vote.height == BlockNumber(2) && vote.proposal_commitment.is_none()
441+
})
438442
.times(1)
439443
.return_once(move |_| {
440444
height2_nil_vote_trigger.send(()).unwrap();
@@ -528,16 +532,16 @@ async fn current_height_round_limit_caching_and_dropping(mut consensus_config: C
528532

529533
// Send votes for round 1. These should be dropped because when state machine is in round 0,
530534
// round 1 > current_round(0) + future_round_limit(0).
531-
send(&mut sender, prevote(Some(Felt::ONE), 1, 1, *PROPOSER_ID)).await;
532-
send(&mut sender, prevote(Some(Felt::ONE), 1, 1, *VALIDATOR_ID_2)).await;
533-
send(&mut sender, precommit(Some(Felt::ONE), 1, 1, *PROPOSER_ID)).await;
534-
send(&mut sender, precommit(Some(Felt::ONE), 1, 1, *VALIDATOR_ID_2)).await;
535+
send(&mut sender, prevote(Some(Felt::ONE), BlockNumber(1), 1, *PROPOSER_ID)).await;
536+
send(&mut sender, prevote(Some(Felt::ONE), BlockNumber(1), 1, *VALIDATOR_ID_2)).await;
537+
send(&mut sender, precommit(Some(Felt::ONE), BlockNumber(1), 1, *PROPOSER_ID)).await;
538+
send(&mut sender, precommit(Some(Felt::ONE), BlockNumber(1), 1, *VALIDATOR_ID_2)).await;
535539

536540
// Send Nil votes for round 0 (current round).
537-
send(&mut sender, prevote(None, 1, 0, *VALIDATOR_ID_2)).await;
538-
send(&mut sender, prevote(None, 1, 0, *PROPOSER_ID)).await;
539-
send(&mut sender, precommit(None, 1, 0, *VALIDATOR_ID_2)).await;
540-
send(&mut sender, precommit(None, 1, 0, *PROPOSER_ID)).await;
541+
send(&mut sender, prevote(None, BlockNumber(1), 0, *VALIDATOR_ID_2)).await;
542+
send(&mut sender, prevote(None, BlockNumber(1), 0, *PROPOSER_ID)).await;
543+
send(&mut sender, precommit(None, BlockNumber(1), 0, *VALIDATOR_ID_2)).await;
544+
send(&mut sender, precommit(None, BlockNumber(1), 0, *PROPOSER_ID)).await;
541545

542546
let mut context = MockTestContext::new();
543547
context.expect_try_sync().returning(|_| false);
@@ -582,10 +586,10 @@ async fn current_height_round_limit_caching_and_dropping(mut consensus_config: C
582586
tokio::spawn(async move {
583587
round1_wait.await.unwrap();
584588
// Send Nil votes from other nodes for round 1.
585-
send(&mut sender_clone1, prevote(None, 1, 1, *VALIDATOR_ID_2)).await;
586-
send(&mut sender_clone1, prevote(None, 1, 1, *PROPOSER_ID)).await;
587-
send(&mut sender_clone1, precommit(None, 1, 1, *VALIDATOR_ID_2)).await;
588-
send(&mut sender_clone1, precommit(None, 1, 1, *PROPOSER_ID)).await;
589+
send(&mut sender_clone1, prevote(None, BlockNumber(1), 1, *VALIDATOR_ID_2)).await;
590+
send(&mut sender_clone1, prevote(None, BlockNumber(1), 1, *PROPOSER_ID)).await;
591+
send(&mut sender_clone1, precommit(None, BlockNumber(1), 1, *VALIDATOR_ID_2)).await;
592+
send(&mut sender_clone1, precommit(None, BlockNumber(1), 1, *PROPOSER_ID)).await;
589593
});
590594

591595
let mut sender_clone2 = sender.clone();
@@ -599,10 +603,12 @@ async fn current_height_round_limit_caching_and_dropping(mut consensus_config: C
599603
)
600604
.await;
601605
// Send votes for round 2.
602-
send(&mut sender_clone2, prevote(Some(Felt::ONE), 1, 2, *PROPOSER_ID)).await;
603-
send(&mut sender_clone2, prevote(Some(Felt::ONE), 1, 2, *VALIDATOR_ID_2)).await;
604-
send(&mut sender_clone2, precommit(Some(Felt::ONE), 1, 2, *PROPOSER_ID)).await;
605-
send(&mut sender_clone2, precommit(Some(Felt::ONE), 1, 2, *VALIDATOR_ID_2)).await;
606+
send(&mut sender_clone2, prevote(Some(Felt::ONE), BlockNumber(1), 2, *PROPOSER_ID)).await;
607+
send(&mut sender_clone2, prevote(Some(Felt::ONE), BlockNumber(1), 2, *VALIDATOR_ID_2))
608+
.await;
609+
send(&mut sender_clone2, precommit(Some(Felt::ONE), BlockNumber(1), 2, *PROPOSER_ID)).await;
610+
send(&mut sender_clone2, precommit(Some(Felt::ONE), BlockNumber(1), 2, *VALIDATOR_ID_2))
611+
.await;
606612
});
607613

608614
// Run height 1 - should reach consensus in round 2 because:
@@ -663,7 +669,7 @@ async fn run_consensus_dynamic_client_updates_validator_between_heights(
663669
let (decision_tx, decision_rx) = oneshot::channel();
664670
context
665671
.expect_decision_reached()
666-
.withf(move |_, votes| votes.first().map(|v| v.height) == Some(2))
672+
.withf(move |_, votes| votes.first().map(|v| v.height) == Some(BlockNumber(2)))
667673
.return_once(move |_, _| {
668674
let _ = decision_tx.send(());
669675
Ok(())
@@ -776,8 +782,8 @@ async fn manager_runs_normally_when_height_is_greater_than_last_voted_height(
776782
vec![TestProposalPart::Init(proposal_init(CURRENT_HEIGHT.0, 0, *PROPOSER_ID))],
777783
)
778784
.await;
779-
send(&mut sender, prevote(Some(Felt::ONE), CURRENT_HEIGHT.0, 0, *PROPOSER_ID)).await;
780-
send(&mut sender, precommit(Some(Felt::ONE), CURRENT_HEIGHT.0, 0, *PROPOSER_ID)).await;
785+
send(&mut sender, prevote(Some(Felt::ONE), CURRENT_HEIGHT, 0, *PROPOSER_ID)).await;
786+
send(&mut sender, precommit(Some(Felt::ONE), CURRENT_HEIGHT, 0, *PROPOSER_ID)).await;
781787

782788
let mut context = MockTestContext::new();
783789
// Sync will never succeed so we will proceed to run consensus (during which try_sync is called
@@ -831,8 +837,8 @@ async fn manager_waits_until_height_passes_last_voted_height(consensus_config: C
831837
vec![TestProposalPart::Init(proposal_init(LAST_VOTED_HEIGHT.0, 0, *PROPOSER_ID))],
832838
)
833839
.await;
834-
send(&mut sender, prevote(Some(Felt::ONE), LAST_VOTED_HEIGHT.0, 0, *PROPOSER_ID)).await;
835-
send(&mut sender, precommit(Some(Felt::ONE), LAST_VOTED_HEIGHT.0, 0, *PROPOSER_ID)).await;
840+
send(&mut sender, prevote(Some(Felt::ONE), LAST_VOTED_HEIGHT, 0, *PROPOSER_ID)).await;
841+
send(&mut sender, precommit(Some(Felt::ONE), LAST_VOTED_HEIGHT, 0, *PROPOSER_ID)).await;
836842

837843
let mut context = MockTestContext::new();
838844
// At the last voted height we expect the manager to halt until it can get the last voted height
@@ -904,7 +910,7 @@ async fn writes_voted_height_to_storage(consensus_config: ConsensusConfig) {
904910
context
905911
.expect_broadcast()
906912
.times(1)
907-
.withf(move |msg: &Vote| msg == &prevote(Some(block_id.0), HEIGHT.0, 0, *VALIDATOR_ID))
913+
.withf(move |msg: &Vote| msg == &prevote(Some(block_id.0), HEIGHT, 0, *VALIDATOR_ID))
908914
.in_sequence(&mut storage_before_broadcast_sequence)
909915
.returning(move |_| {
910916
if let Some(tx) = prevote_tx_for_callback.lock().unwrap().take() {
@@ -929,7 +935,7 @@ async fn writes_voted_height_to_storage(consensus_config: ConsensusConfig) {
929935
context
930936
.expect_broadcast()
931937
.times(1)
932-
.withf(move |msg: &Vote| msg == &precommit(Some(block_id.0), HEIGHT.0, 0, *VALIDATOR_ID))
938+
.withf(move |msg: &Vote| msg == &precommit(Some(block_id.0), HEIGHT, 0, *VALIDATOR_ID))
933939
.in_sequence(&mut storage_before_broadcast_sequence)
934940
.returning(move |_| {
935941
if let Some(tx) = precommit_tx_for_callback.lock().unwrap().take() {
@@ -967,24 +973,20 @@ async fn writes_voted_height_to_storage(consensus_config: ConsensusConfig) {
967973
prevote_rx.await.unwrap();
968974

969975
// Now send other prevotes to reach quorum
970-
send(&mut sender_clone, prevote(Some(block_id_for_votes.0), HEIGHT.0, 0, *PROPOSER_ID))
976+
send(&mut sender_clone, prevote(Some(block_id_for_votes.0), HEIGHT, 0, *PROPOSER_ID)).await;
977+
send(&mut sender_clone, prevote(Some(block_id_for_votes.0), HEIGHT, 0, *VALIDATOR_ID_2))
971978
.await;
972-
send(&mut sender_clone, prevote(Some(block_id_for_votes.0), HEIGHT.0, 0, *VALIDATOR_ID_2))
973-
.await;
974-
send(&mut sender_clone, prevote(Some(block_id_for_votes.0), HEIGHT.0, 0, *VALIDATOR_ID_3))
979+
send(&mut sender_clone, prevote(Some(block_id_for_votes.0), HEIGHT, 0, *VALIDATOR_ID_3))
975980
.await;
976981

977982
// Wait for validator to broadcast precommit after reaching prevote quorum
978983
precommit_rx.await.unwrap();
979984

980985
// Now send other precommits to reach decision
981-
send(&mut sender_clone, precommit(Some(block_id_for_votes.0), HEIGHT.0, 0, *PROPOSER_ID))
986+
send(&mut sender_clone, precommit(Some(block_id_for_votes.0), HEIGHT, 0, *PROPOSER_ID))
987+
.await;
988+
send(&mut sender_clone, precommit(Some(block_id_for_votes.0), HEIGHT, 0, *VALIDATOR_ID_2))
982989
.await;
983-
send(
984-
&mut sender_clone,
985-
precommit(Some(block_id_for_votes.0), HEIGHT.0, 0, *VALIDATOR_ID_2),
986-
)
987-
.await;
988990
});
989991

990992
// Run height - this should trigger storage writes before broadcasts

crates/apollo_consensus/src/simulation_network_receiver_test.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use apollo_network_types::network_types::BroadcastedMessageMetadata;
66
use apollo_protobuf::consensus::Vote;
77
use apollo_test_utils::{get_rng, GetTestInstance};
88
use futures::{SinkExt, StreamExt};
9+
use starknet_api::block::BlockNumber;
910
use test_case::test_case;
1011

1112
use super::NetworkReceiver;
@@ -31,7 +32,10 @@ async fn test_invalid(distinct_messages: bool) {
3132
let mut invalid_messages = 0;
3233

3334
for height in 0..1000 {
34-
let msg = Vote { height: if distinct_messages { height } else { 0 }, ..Default::default() };
35+
let msg = Vote {
36+
height: if distinct_messages { BlockNumber(height) } else { BlockNumber(0) },
37+
..Default::default()
38+
};
3539
let broadcasted_message_metadata =
3640
BroadcastedMessageMetadata::get_test_instance(&mut get_rng());
3741
mock_network
@@ -62,7 +66,10 @@ async fn test_drops(distinct_messages: bool) {
6266
let mut num_received = 0;
6367

6468
for height in 0..1000 {
65-
let msg = Vote { height: if distinct_messages { height } else { 0 }, ..Default::default() };
69+
let msg = Vote {
70+
height: if distinct_messages { BlockNumber(height) } else { BlockNumber(0) },
71+
..Default::default()
72+
};
6673
let broadcasted_message_metadata =
6774
BroadcastedMessageMetadata::get_test_instance(&mut get_rng());
6875
mock_network

0 commit comments

Comments
 (0)