1pub mod shards;
55pub mod types;
56pub(crate) mod validation;
57
58mod variant;
59pub use variant::Coding;
60
61mod marshaled;
62pub use marshaled::{Marshaled, MarshaledConfig};
63
64#[cfg(test)]
65mod tests {
66 use crate::{
67 marshal::{
68 ancestry::BlockProvider,
69 coding::{
70 shards,
71 types::{coding_config_for_participants, hash_context, CodedBlock},
72 Coding, Marshaled, MarshaledConfig,
73 },
74 config::{Config, Start},
75 core,
76 mocks::{
77 application::Application,
78 harness::{
79 self, default_leader, genesis_commitment, make_coding_block,
80 setup_network_links, setup_network_with_participants, CodingB, CodingCtx,
81 CodingHarness, EmptyProvider, TestHarness, BLOCKS_PER_EPOCH, D, K, LINK,
82 NAMESPACE, NUM_VALIDATORS, QUORUM, S, TEST_QUOTA, UNRELIABLE_LINK, V,
83 },
84 verifying::{GatedVerifyingApp, MockVerifyingApp},
85 },
86 resolver::handler,
87 },
88 simplex::{
89 scheme::bls12381_threshold::vrf as bls12381_threshold_vrf, types::Proposal, Plan,
90 },
91 types::{coding::Commitment, Epoch, Epocher, FixedEpocher, Height, Round, View, ViewDelta},
92 Automaton, Block, CertifiableAutomaton, CertifiableBlock, Relay,
93 };
94 use bytes::Bytes;
95 use commonware_actor::{mailbox, Feedback};
96 use commonware_codec::{Encode, FixedSize};
97 use commonware_coding::{CodecConfig, Config as CodingConfig, ReedSolomon};
98 use commonware_cryptography::{
99 certificate::{mocks::Fixture, ConstantProvider, Verifier as _},
100 sha256::Sha256,
101 Committable, Digestible, Hasher,
102 };
103 use commonware_macros::{select, test_group, test_traced};
104 use commonware_p2p::Recipients;
105 use commonware_parallel::Sequential;
106 use commonware_resolver::{Delivery, Fetch, Resolver, TargetedResolver};
107 use commonware_runtime::{
108 buffer::paged::CacheRef, deterministic, Clock, Metrics, Runner, Supervisor as _,
109 };
110 use commonware_storage::archive::immutable;
111 use commonware_utils::{
112 channel::oneshot, sync::Mutex, vec::NonEmptyVec, NZUsize, NZU16, NZU64,
113 };
114 use std::{sync::Arc, time::Duration};
115
116 type TestCodingVariant = Coding<CodingB, ReedSolomon<Sha256>, Sha256, K>;
117 type TestCodedBlock = CodedBlock<CodingB, ReedSolomon<Sha256>, Sha256>;
118 type CodingSendRecord = (Round, Arc<TestCodedBlock>, Recipients<K>);
119
120 const GENESIS_CODING_CONFIG: CodingConfig = CodingConfig {
122 minimum_shards: NZU16!(1),
123 extra_shards: NZU16!(1),
124 };
125
126 #[test]
127 fn mailbox_provides_application_blocks() {
128 fn assert_provider<P: BlockProvider<Block = CodingB>>() {}
129 assert_provider::<core::Mailbox<S, TestCodingVariant>>();
130 }
131
132 #[derive(Clone, Default)]
134 struct RecordingCodingBuffer {
135 digest_subscriptions: Arc<Mutex<Vec<oneshot::Sender<Arc<TestCodedBlock>>>>>,
136 commitment_subscriptions: Arc<Mutex<Vec<oneshot::Sender<Arc<TestCodedBlock>>>>>,
137 sends: Arc<Mutex<Vec<CodingSendRecord>>>,
138 }
139
140 impl RecordingCodingBuffer {
141 fn subscription_count(&self) -> usize {
142 self.digest_subscriptions.lock().len() + self.commitment_subscriptions.lock().len()
143 }
144
145 fn commitment_subscription_count(&self) -> usize {
146 self.commitment_subscriptions.lock().len()
147 }
148 }
149
150 impl core::Buffer<TestCodingVariant> for RecordingCodingBuffer {
151 type PublicKey = K;
152
153 async fn find_by_digest(&self, _digest: D) -> Option<Arc<TestCodedBlock>> {
154 None
155 }
156
157 async fn find_by_commitment(&self, _commitment: Commitment) -> Option<Arc<TestCodedBlock>> {
158 None
159 }
160
161 fn subscribe_by_digest(
162 &self,
163 _digest: D,
164 ) -> Option<oneshot::Receiver<Arc<TestCodedBlock>>> {
165 let (sender, receiver) = oneshot::channel();
166 self.digest_subscriptions.lock().push(sender);
167 Some(receiver)
168 }
169
170 fn subscribe_by_commitment(
171 &self,
172 _commitment: Commitment,
173 ) -> Option<oneshot::Receiver<Arc<TestCodedBlock>>> {
174 let (sender, receiver) = oneshot::channel();
175 self.commitment_subscriptions.lock().push(sender);
176 Some(receiver)
177 }
178
179 fn finalized(&self, _commitment: Commitment) {}
180
181 fn send(&self, round: Round, block: Arc<TestCodedBlock>, recipients: Recipients<K>) {
182 self.sends.lock().push((round, block, recipients));
183 }
184 }
185
186 type CodingFetchRecord = Fetch<handler::Key<Commitment>, handler::Annotation>;
187 type CodingTargetedFetch = (handler::Key<Commitment>, NonEmptyVec<K>);
188
189 #[derive(Clone, Default)]
191 struct RecordingResolver {
192 fetches: Arc<Mutex<Vec<CodingFetchRecord>>>,
193 targeted: Arc<Mutex<Vec<CodingTargetedFetch>>>,
194 auto_delivery: Arc<Mutex<Option<Bytes>>>,
195 delivery_responses: Arc<Mutex<Vec<oneshot::Receiver<bool>>>>,
196 sender: Option<mailbox::Sender<handler::Message<Commitment>>>,
197 }
198
199 impl RecordingResolver {
200 fn holding(metrics: impl Metrics) -> (handler::Receiver<Commitment>, Self) {
201 let (sender, receiver) = mailbox::new(metrics, NZUsize!(100));
202 (
203 handler::Receiver::new(receiver),
204 Self {
205 fetches: Arc::new(Mutex::new(Vec::new())),
206 targeted: Arc::new(Mutex::new(Vec::new())),
207 auto_delivery: Arc::new(Mutex::new(None)),
208 delivery_responses: Arc::new(Mutex::new(Vec::new())),
209 sender: Some(sender),
210 },
211 )
212 }
213
214 fn record_fetch(&self, fetch: CodingFetchRecord) {
215 self.fetches.lock().push(fetch.clone());
216 let Some(value) = self.auto_delivery.lock().take() else {
217 return;
218 };
219 let Some(sender) = &self.sender else {
220 return;
221 };
222 let (response, response_rx) = oneshot::channel();
223 self.delivery_responses.lock().push(response_rx);
224 let _ = sender.enqueue(handler::Message::Deliver {
225 delivery: Delivery {
226 key: fetch.key,
227 subscribers: NonEmptyVec::new((fetch.subscriber, tracing::Span::none())),
228 },
229 value,
230 response,
231 });
232 }
233
234 fn respond_to_next_fetch(&self, value: Bytes) {
235 let replaced = self.auto_delivery.lock().replace(value);
236 assert!(
237 replaced.is_none(),
238 "recording resolver already has an automatic delivery"
239 );
240 }
241
242 async fn wait_for_delivery_response(&self) -> bool {
243 let response = self
244 .delivery_responses
245 .lock()
246 .pop()
247 .expect("delivery response missing");
248 response.await.expect("delivery response sender dropped")
249 }
250
251 fn fetches(&self) -> Vec<CodingFetchRecord> {
252 self.fetches.lock().clone()
253 }
254
255 fn targeted(&self) -> Vec<CodingTargetedFetch> {
256 self.targeted.lock().clone()
257 }
258 }
259
260 impl Resolver for RecordingResolver {
261 type Key = handler::Key<Commitment>;
262 type Subscriber = handler::Annotation;
263
264 fn fetch<F>(&mut self, fetch: F) -> Feedback
265 where
266 F: Into<Fetch<Self::Key, Self::Subscriber>> + Send,
267 {
268 self.record_fetch(fetch.into());
269 Feedback::Ok
270 }
271
272 fn fetch_all<F>(&mut self, fetches: Vec<F>) -> Feedback
273 where
274 F: Into<Fetch<Self::Key, Self::Subscriber>> + Send,
275 {
276 for fetch in fetches {
277 self.record_fetch(fetch.into());
278 }
279 Feedback::Ok
280 }
281
282 fn retain(
283 &mut self,
284 _predicate: impl Fn(&Self::Key, &Self::Subscriber) -> bool + Send + 'static,
285 ) -> Feedback {
286 Feedback::Ok
287 }
288 }
289
290 impl TargetedResolver for RecordingResolver {
291 type PublicKey = K;
292
293 fn fetch_targeted(
294 &mut self,
295 fetch: impl Into<Fetch<Self::Key, Self::Subscriber>> + Send,
296 targets: NonEmptyVec<Self::PublicKey>,
297 ) -> Feedback {
298 self.targeted.lock().push((fetch.into().key, targets));
299 Feedback::Ok
300 }
301
302 fn fetch_all_targeted<F>(
303 &mut self,
304 fetches: Vec<(F, NonEmptyVec<Self::PublicKey>)>,
305 ) -> Feedback
306 where
307 F: Into<Fetch<Self::Key, Self::Subscriber>> + Send,
308 {
309 let mut targeted = self.targeted.lock();
310 for (fetch, targets) in fetches {
311 targeted.push((fetch.into().key, targets));
312 }
313 Feedback::Ok
314 }
315 }
316
317 async fn start_coding_actor_with_recording(
318 context: deterministic::Context,
319 partition_prefix: &str,
320 provider: ConstantProvider<S, Epoch>,
321 buffer: RecordingCodingBuffer,
322 ) -> (
323 core::Mailbox<S, TestCodingVariant>,
324 RecordingResolver,
325 commonware_runtime::Handle<()>,
326 ) {
327 let config = Config {
328 provider,
329 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
330 start: Start::Genesis(CodingHarness::genesis_block(NUM_VALIDATORS as u16)),
331 mailbox_size: NZUsize!(100),
332 view_retention_timeout: ViewDelta::new(10),
333 max_repair: NZUsize!(10),
334 max_pending_acks: NZUsize!(1),
335 block_codec_config: (),
336 partition_prefix: partition_prefix.to_string(),
337 prunable_items_per_section: NZU64!(10),
338 replay_buffer: NZUsize!(1024),
339 key_write_buffer: NZUsize!(1024),
340 value_write_buffer: NZUsize!(1024),
341 page_cache: CacheRef::from_pooler(
342 &context,
343 harness::PAGE_SIZE,
344 harness::PAGE_CACHE_SIZE,
345 ),
346 strategy: Sequential,
347 };
348
349 let finalizations_by_height = immutable::Archive::init(
350 context.child("finalizations_by_height"),
351 immutable::Config {
352 metadata_partition: format!("{partition_prefix}-finalizations-by-height-metadata"),
353 freezer_table_partition: format!(
354 "{partition_prefix}-finalizations-by-height-freezer-table"
355 ),
356 freezer_table_initial_size: 64,
357 freezer_table_resize_frequency: 10,
358 freezer_table_resize_chunk_size: 10,
359 freezer_key_partition: format!(
360 "{partition_prefix}-finalizations-by-height-freezer-key"
361 ),
362 freezer_key_page_cache: config.page_cache.clone(),
363 freezer_value_partition: format!(
364 "{partition_prefix}-finalizations-by-height-freezer-value"
365 ),
366 freezer_value_target_size: 1024,
367 freezer_value_compression: None,
368 ordinal_partition: format!("{partition_prefix}-finalizations-by-height-ordinal"),
369 items_per_section: NZU64!(10),
370 codec_config: S::certificate_codec_config_unbounded(),
371 replay_buffer: config.replay_buffer,
372 freezer_key_write_buffer: config.key_write_buffer,
373 freezer_value_write_buffer: config.value_write_buffer,
374 ordinal_write_buffer: config.key_write_buffer,
375 },
376 )
377 .await
378 .expect("failed to initialize finalizations by height archive");
379
380 let finalized_blocks = immutable::Archive::init(
381 context.child("finalized_blocks"),
382 immutable::Config {
383 metadata_partition: format!("{partition_prefix}-finalized_blocks-metadata"),
384 freezer_table_partition: format!(
385 "{partition_prefix}-finalized_blocks-freezer-table"
386 ),
387 freezer_table_initial_size: 64,
388 freezer_table_resize_frequency: 10,
389 freezer_table_resize_chunk_size: 10,
390 freezer_key_partition: format!("{partition_prefix}-finalized_blocks-freezer-key"),
391 freezer_key_page_cache: config.page_cache.clone(),
392 freezer_value_partition: format!(
393 "{partition_prefix}-finalized_blocks-freezer-value"
394 ),
395 freezer_value_target_size: 1024,
396 freezer_value_compression: None,
397 ordinal_partition: format!("{partition_prefix}-finalized_blocks-ordinal"),
398 items_per_section: NZU64!(10),
399 codec_config: config.block_codec_config,
400 replay_buffer: config.replay_buffer,
401 freezer_key_write_buffer: config.key_write_buffer,
402 freezer_value_write_buffer: config.value_write_buffer,
403 ordinal_write_buffer: config.key_write_buffer,
404 },
405 )
406 .await
407 .expect("failed to initialize finalized blocks archive");
408
409 let (actor, mailbox, _) = core::Actor::init(
410 context.child("actor"),
411 finalizations_by_height,
412 finalized_blocks,
413 config,
414 )
415 .await;
416 let (resolver_rx, resolver) = RecordingResolver::holding(context.child("resolver"));
417 let actor_handle = actor.start(
418 Application::<CodingB>::default(),
419 buffer,
420 (resolver_rx, resolver.clone()),
421 );
422 (mailbox, resolver, actor_handle)
423 }
424
425 async fn start_shard_mailbox(
426 context: deterministic::Context,
427 participants: Vec<K>,
428 provider: ConstantProvider<S, Epoch>,
429 ) -> shards::Mailbox<CodingB, ReedSolomon<Sha256>, Sha256, K> {
430 let me = participants[0].clone();
431 let oracle =
432 setup_network_with_participants(context.child("network"), NZUsize!(1), participants)
433 .await;
434 let control = oracle.control(me.clone());
435 let shard_config: shards::Config<_, _, _, _, _, Sha256, _, _> = shards::Config {
436 scheme_provider: provider,
437 blocker: control.clone(),
438 shard_codec_cfg: CodecConfig {
439 maximum_shard_size: 1024 * 1024,
440 },
441 block_codec_cfg: (),
442 strategy: Sequential,
443 mailbox_size: NZUsize!(10),
444 peer_buffer_size: NZUsize!(64),
445 background_channel_capacity: NZUsize!(1024),
446 peer_provider: oracle.manager(),
447 };
448 let (shard_engine, shard_mailbox) =
449 shards::Engine::new(context.child("shards"), shard_config);
450 let network = control.register(0, TEST_QUOTA).await.unwrap();
451 shard_engine.start(network);
452 shard_mailbox
453 }
454
455 fn genesis_block() -> CodingB {
456 let genesis_ctx = CodingCtx {
457 round: Round::zero(),
458 leader: default_leader(),
459 parent: (View::zero(), genesis_commitment()),
460 };
461 make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0)
462 }
463
464 fn genesis_coding_commitment<H: Hasher, B: CertifiableBlock>(block: &B) -> Commitment {
465 Commitment::from((
466 block.digest(),
467 block.digest(),
468 hash_context::<H, _>(&block.context()),
469 GENESIS_CODING_CONFIG,
470 ))
471 }
472
473 fn missing_candidate(me: K) -> (CodingCtx, TestCodedBlock) {
474 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
475 let genesis = genesis_block();
476 let genesis_parent_commitment = genesis_coding_commitment::<Sha256, _>(&genesis);
477 let round = Round::new(Epoch::zero(), View::new(1));
478 let candidate_ctx = CodingCtx {
479 round,
480 leader: me,
481 parent: (View::zero(), genesis_parent_commitment),
482 };
483 let candidate =
484 make_coding_block(candidate_ctx.clone(), genesis.digest(), Height::new(1), 100);
485 let coded_candidate: TestCodedBlock =
486 CodedBlock::new(candidate, coding_config, &Sequential);
487 (candidate_ctx, coded_candidate)
488 }
489
490 #[test_traced("WARN")]
491 fn test_coding_notarized_delivery_rejects_dishonest_payload_config() {
492 let runner = deterministic::Runner::timed(Duration::from_secs(30));
493 runner.start(|mut context| async move {
494 let Fixture {
495 participants,
496 schemes,
497 ..
498 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
499 let provider = ConstantProvider::new(schemes[0].clone());
500 let honest_config = coding_config_for_participants(NUM_VALIDATORS as u16);
501 let dishonest_config = coding_config_for_participants((NUM_VALIDATORS + 3) as u16);
502 assert_ne!(honest_config, dishonest_config);
503
504 let buffer = RecordingCodingBuffer::default();
505 let (marshal, resolver, _actor_handle) = start_coding_actor_with_recording(
506 context.child("actor_stack"),
507 "coding-dishonest-payload-config",
508 provider,
509 buffer,
510 )
511 .await;
512 let resolver_tx = resolver
513 .sender
514 .clone()
515 .expect("recording resolver should keep its sender");
516
517 let genesis = genesis_block();
518 let round = Round::new(Epoch::zero(), View::new(1));
519 let height = Height::new(1);
520 let candidate_ctx = CodingCtx {
521 round,
522 leader: participants[0].clone(),
523 parent: (
524 View::zero(),
525 genesis_coding_commitment::<Sha256, _>(&genesis),
526 ),
527 };
528 let candidate = make_coding_block(candidate_ctx, genesis.digest(), height, 100);
529 let dishonest_block: TestCodedBlock =
530 CodedBlock::new(candidate.clone(), dishonest_config, &Sequential);
531 let proposal = Proposal {
532 round,
533 parent: View::zero(),
534 payload: dishonest_block.commitment(),
535 };
536 let notarization = CodingHarness::make_notarization(proposal, &schemes, QUORUM);
537
538 let (response, response_rx) = oneshot::channel();
539 assert!(resolver_tx
540 .enqueue(handler::Message::Deliver {
541 delivery: Delivery {
542 key: handler::Key::Notarized { round },
543 subscribers: NonEmptyVec::new((
544 handler::Annotation::Notarization { round },
545 tracing::Span::none(),
546 )),
547 },
548 value: (notarization, dishonest_block).encode(),
549 response,
550 })
551 .accepted());
552 assert!(
553 !response_rx.await.unwrap(),
554 "notarized delivery should reject a dishonest coding config"
555 );
556
557 context.sleep(Duration::from_millis(100)).await;
558 assert!(
559 marshal.get_block(height).await.is_none(),
560 "dishonest deliveries must not store a finalized block"
561 );
562 assert!(
563 marshal.get_finalization(height).await.is_none(),
564 "dishonest deliveries must not archive a finalization"
565 );
566 });
567 }
568
569 #[test_traced("WARN")]
570 fn test_coding_block_provider_parent_fetches_by_commitment() {
571 let runner = deterministic::Runner::timed(Duration::from_secs(30));
572 runner.start(|mut context| async move {
573 let Fixture {
574 participants,
575 schemes,
576 ..
577 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
578 let provider = ConstantProvider::new(schemes[0].clone());
579 let buffer = RecordingCodingBuffer::default();
580 let (marshal, _resolver, _actor_handle) = start_coding_actor_with_recording(
581 context.child("actor_stack"),
582 "coding-provider-parent-commitment",
583 provider,
584 buffer.clone(),
585 )
586 .await;
587
588 let (parent_ctx, parent) = missing_candidate(participants[0].clone());
589 let child_ctx = CodingCtx {
590 round: Round::new(Epoch::zero(), View::new(2)),
591 leader: participants[0].clone(),
592 parent: (parent_ctx.round.view(), parent.commitment()),
593 };
594 let child = make_coding_block(child_ctx, parent.digest(), Height::new(2), 200);
595 let subscription = marshal.subscribe_parent(&child);
596
597 context.sleep(Duration::from_millis(100)).await;
598 assert_eq!(
599 buffer.commitment_subscription_count(),
600 1,
601 "parent walkback should use the coding parent commitment"
602 );
603 drop(subscription);
604 });
605 }
606
607 #[test_traced("WARN")]
608 fn test_coding_verify_missing_candidate_waits_without_fetching() {
609 let runner = deterministic::Runner::timed(Duration::from_secs(30));
610 runner.start(|mut context| async move {
611 let Fixture {
612 participants,
613 schemes,
614 ..
615 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
616 let provider = ConstantProvider::new(schemes[0].clone());
617 let me = participants[0].clone();
618 let buffer = RecordingCodingBuffer::default();
619 let (marshal, resolver, _actor_handle) = start_coding_actor_with_recording(
620 context.child("actor_stack"),
621 "coding-verify-missing-candidate",
622 provider.clone(),
623 buffer.clone(),
624 )
625 .await;
626 let shards =
627 start_shard_mailbox(context.child("shard_stack"), participants, provider.clone())
628 .await;
629 let (candidate_ctx, candidate) = missing_candidate(me);
630 let commitment = candidate.commitment();
631
632 let cfg = MarshaledConfig {
633 application: MockVerifyingApp::<CodingB, S>::new(),
634 marshal,
635 shards,
636 scheme_provider: provider,
637 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
638 strategy: Sequential,
639 };
640 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
641
642 let verify_rx = marshaled.verify(candidate_ctx, commitment).await;
643 context.sleep(Duration::from_millis(100)).await;
644
645 assert!(
646 buffer.subscription_count() > 0,
647 "missing candidate should register a local buffer wait"
648 );
649 assert!(
650 resolver.fetches().is_empty(),
651 "missing candidate verify must not fetch from peers"
652 );
653 assert!(
654 resolver.targeted().is_empty(),
655 "missing candidate verify must not issue targeted fetches"
656 );
657 drop(verify_rx);
658 });
659 }
660
661 #[test_traced("WARN")]
667 fn test_coding_certify_missing_candidate_fetches_by_round() {
668 let runner = deterministic::Runner::timed(Duration::from_secs(30));
669 runner.start(|mut context| async move {
670 let Fixture {
671 participants,
672 schemes,
673 ..
674 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
675 let provider = ConstantProvider::new(schemes[0].clone());
676 let me = participants[0].clone();
677 let buffer = RecordingCodingBuffer::default();
678 let (marshal, resolver, _actor_handle) = start_coding_actor_with_recording(
679 context.child("actor_stack"),
680 "coding-certify-missing-candidate",
681 provider.clone(),
682 buffer.clone(),
683 )
684 .await;
685 let shards =
686 start_shard_mailbox(context.child("shard_stack"), participants, provider.clone())
687 .await;
688
689 let cfg = MarshaledConfig {
690 application: MockVerifyingApp::<CodingB, S>::new(),
691 marshal,
692 shards,
693 scheme_provider: provider,
694 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
695 strategy: Sequential,
696 };
697 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
698
699 let (candidate_ctx, candidate) = missing_candidate(me);
700 let commitment = candidate.commitment();
701 let round = candidate_ctx.round;
702 let proposal = Proposal::new(round, View::zero(), commitment);
703 let notarization = CodingHarness::make_notarization(proposal, &schemes, QUORUM);
704 resolver.respond_to_next_fetch((notarization, candidate).encode());
705 let certify_rx = marshaled.certify(round, commitment).await;
706
707 let result = certify_rx.await.expect("certify result missing");
708 assert!(result, "fetched notarized candidate should certify");
709 assert!(
710 resolver.wait_for_delivery_response().await,
711 "notarized delivery should validate"
712 );
713 assert!(
714 resolver.fetches().iter().any(|fetch| matches!(
715 (&fetch.key, &fetch.subscriber),
716 (
717 handler::Key::Notarized { round: request_round },
718 handler::Annotation::Notarization { round: subscriber_round },
719 ) if *request_round == round && *subscriber_round == round
720 )),
721 "certify should fetch notarized block by round"
722 );
723
724 assert!(
725 buffer.subscription_count() > 0,
726 "missing candidate should register a local buffer wait"
727 );
728 assert!(
729 resolver.targeted().is_empty(),
730 "missing candidate certify must not issue targeted fetches"
731 );
732 });
733 }
734
735 #[test_traced("WARN")]
736 fn test_coding_certify_pending_verify_fetches_by_round() {
737 let runner = deterministic::Runner::timed(Duration::from_secs(30));
738 runner.start(|mut context| async move {
739 let Fixture {
740 participants,
741 schemes,
742 ..
743 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
744 let provider = ConstantProvider::new(schemes[0].clone());
745 let me = participants[0].clone();
746 let buffer = RecordingCodingBuffer::default();
747 let (marshal, resolver, _actor_handle) = start_coding_actor_with_recording(
748 context.child("actor_stack"),
749 "coding-certify-pending-verify",
750 provider.clone(),
751 buffer,
752 )
753 .await;
754 let shards =
755 start_shard_mailbox(context.child("shard_stack"), participants, provider.clone())
756 .await;
757
758 let cfg = MarshaledConfig {
759 application: MockVerifyingApp::<CodingB, S>::new(),
760 marshal,
761 shards,
762 scheme_provider: provider,
763 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
764 strategy: Sequential,
765 };
766 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
767
768 let (candidate_ctx, candidate) = missing_candidate(me);
769 let commitment = candidate.commitment();
770 let round = candidate_ctx.round;
771 let _verify_rx = marshaled.verify(candidate_ctx, commitment).await;
772
773 let proposal = Proposal::new(round, View::zero(), commitment);
774 let notarization = CodingHarness::make_notarization(proposal, &schemes, QUORUM);
775 resolver.respond_to_next_fetch((notarization, candidate).encode());
776 let certify_rx = marshaled.certify(round, commitment).await;
777
778 let result = certify_rx.await.expect("certify result missing");
779 assert!(
780 result,
781 "pending verify should complete after certification recovery"
782 );
783 assert!(
784 resolver.wait_for_delivery_response().await,
785 "notarized delivery should validate"
786 );
787 assert!(
788 resolver.fetches().iter().any(|fetch| matches!(
789 (&fetch.key, &fetch.subscriber),
790 (
791 handler::Key::Notarized { round: request_round },
792 handler::Annotation::Notarization { round: subscriber_round },
793 ) if *request_round == round && *subscriber_round == round
794 )),
795 "certify should recover a pending verify by notarized round"
796 );
797 assert!(
798 resolver.targeted().is_empty(),
799 "certify recovery must not issue targeted fetches"
800 );
801 });
802 }
803
804 #[test_group("slow")]
805 #[test_traced("WARN")]
806 fn test_coding_finalize_good_links() {
807 for seed in 0..5 {
808 let r1 = harness::finalize::<CodingHarness>(seed, LINK, false);
809 let r2 = harness::finalize::<CodingHarness>(seed, LINK, false);
810 assert_eq!(r1, r2);
811 }
812 }
813
814 #[test_group("slow")]
815 #[test_traced("WARN")]
816 fn test_coding_finalize_bad_links() {
817 for seed in 0..5 {
818 let r1 = harness::finalize::<CodingHarness>(seed, UNRELIABLE_LINK, false);
819 let r2 = harness::finalize::<CodingHarness>(seed, UNRELIABLE_LINK, false);
820 assert_eq!(r1, r2);
821 }
822 }
823
824 #[test_group("slow")]
825 #[test_traced("WARN")]
826 fn test_coding_finalize_good_links_quorum_sees_finalization() {
827 for seed in 0..5 {
828 let r1 = harness::finalize::<CodingHarness>(seed, LINK, true);
829 let r2 = harness::finalize::<CodingHarness>(seed, LINK, true);
830 assert_eq!(r1, r2);
831 }
832 }
833
834 #[test_group("slow")]
835 #[test_traced("WARN")]
836 fn test_coding_finalize_bad_links_quorum_sees_finalization() {
837 for seed in 0..5 {
838 let r1 = harness::finalize::<CodingHarness>(seed, UNRELIABLE_LINK, true);
839 let r2 = harness::finalize::<CodingHarness>(seed, UNRELIABLE_LINK, true);
840 assert_eq!(r1, r2);
841 }
842 }
843
844 #[test_group("slow")]
845 #[test_traced("WARN")]
846 fn test_coding_hailstorm_restarts() {
847 for seed in 0..2 {
848 let r1 = harness::hailstorm::<CodingHarness>(seed, 4, 4, 1, LINK);
849 let r2 = harness::hailstorm::<CodingHarness>(seed, 4, 4, 1, LINK);
850 assert_eq!(r1, r2);
851 }
852 }
853
854 #[test_group("slow")]
855 #[test_traced("WARN")]
856 fn test_coding_hailstorm_multi_restarts() {
857 for seed in 0..2 {
858 let r1 = harness::hailstorm::<CodingHarness>(seed, 4, 4, 2, LINK);
859 let r2 = harness::hailstorm::<CodingHarness>(seed, 4, 4, 2, LINK);
860 assert_eq!(r1, r2);
861 }
862 }
863
864 #[test_traced("WARN")]
865 fn test_coding_ack_pipeline_backlog() {
866 harness::ack_pipeline_backlog::<CodingHarness>();
867 }
868
869 #[test_traced("WARN")]
870 fn test_coding_ack_pipeline_backlog_persists_on_restart() {
871 harness::ack_pipeline_backlog_persists_on_restart::<CodingHarness>();
872 }
873
874 #[test_traced("WARN")]
875 fn test_coding_genesis_emitted_once() {
876 harness::genesis_emitted_once::<CodingHarness>();
877 }
878
879 #[test_traced("WARN")]
880 fn test_coding_proposed_success_implies_recoverable_after_restart() {
881 harness::proposed_success_implies_recoverable_after_restart::<CodingHarness>(0..16);
882 }
883
884 #[test_traced("WARN")]
885 fn test_coding_verified_success_implies_recoverable_after_restart() {
886 harness::verified_success_implies_recoverable_after_restart::<CodingHarness>(0..16);
887 }
888
889 #[test_traced("WARN")]
890 fn test_coding_certified_success_implies_recoverable_after_restart() {
891 harness::certified_success_implies_recoverable_after_restart::<CodingHarness>(0..16);
892 }
893
894 #[test_traced("WARN")]
895 fn test_coding_delivery_visibility_implies_recoverable_after_restart() {
896 harness::delivery_visibility_implies_recoverable_after_restart::<CodingHarness>(0..16);
897 }
898
899 #[test_traced("WARN")]
900 fn test_coding_sync_height_floor() {
901 harness::sync_height_floor::<CodingHarness>();
902 }
903
904 #[test_traced("WARN")]
905 fn test_coding_prune_finalized_archives() {
906 harness::prune_finalized_archives::<CodingHarness>();
907 }
908
909 #[test_traced("WARN")]
910 fn test_coding_rejects_block_delivery_below_floor() {
911 harness::reject_stale_block_delivery_after_floor_update::<CodingHarness>();
912 }
913
914 #[test_traced("WARN")]
915 fn test_coding_commitment_fetch_height_hint_mismatch_wakes_subscriber() {
916 harness::commitment_fetch_height_hint_mismatch_wakes_subscriber::<CodingHarness>();
917 }
918
919 #[test_traced("WARN")]
920 fn test_coding_subscribe_basic_block_delivery() {
921 harness::subscribe_basic_block_delivery::<CodingHarness>();
922 }
923
924 #[test_traced("WARN")]
925 fn test_coding_subscribe_multiple_subscriptions() {
926 harness::subscribe_multiple_subscriptions::<CodingHarness>();
927 }
928
929 #[test_traced("WARN")]
930 fn test_coding_subscribe_canceled_subscriptions() {
931 harness::subscribe_canceled_subscriptions::<CodingHarness>();
932 }
933
934 #[test_traced("WARN")]
935 fn test_coding_subscribe_blocks_from_different_sources() {
936 harness::subscribe_blocks_from_different_sources::<CodingHarness>();
937 }
938
939 #[test_traced("WARN")]
940 fn test_coding_get_info_basic_queries_present_and_missing() {
941 harness::get_info_basic_queries_present_and_missing::<CodingHarness>();
942 }
943
944 #[test_traced("WARN")]
945 fn test_coding_get_info_latest_progression_multiple_finalizations() {
946 harness::get_info_latest_progression_multiple_finalizations::<CodingHarness>();
947 }
948
949 #[test_traced("WARN")]
950 fn test_coding_get_block_by_height_and_latest() {
951 harness::get_block_by_height_and_latest::<CodingHarness>();
952 }
953
954 #[test_traced("WARN")]
955 fn test_coding_get_block_by_commitment_from_sources_and_missing() {
956 harness::get_block_by_commitment_from_sources_and_missing::<CodingHarness>();
957 }
958
959 #[test_traced("WARN")]
960 fn test_coding_get_finalization_by_height() {
961 harness::get_finalization_by_height::<CodingHarness>();
962 }
963
964 #[test_traced("WARN")]
965 fn test_coding_hint_finalized_triggers_fetch() {
966 harness::hint_finalized_triggers_fetch::<CodingHarness>();
967 }
968
969 #[test_traced("WARN")]
970 fn test_coding_ancestry_stream() {
971 harness::ancestry_stream::<CodingHarness>();
972 }
973
974 #[test_traced("WARN")]
975 fn test_coding_finalize_same_height_different_views() {
976 harness::finalize_same_height_different_views::<CodingHarness>();
977 }
978
979 #[test_traced("WARN")]
980 fn test_coding_certify_persists_equivocated_block() {
981 harness::certify_persists_equivocated_block::<CodingHarness>();
982 }
983
984 #[test_traced("WARN")]
985 fn test_coding_verified_after_restart_reverify_same_round_implies_recoverable() {
986 harness::verified_after_restart_reverify_same_round_implies_recoverable::<CodingHarness>();
987 }
988
989 #[test_traced("WARN")]
990 fn test_coding_certify_after_restart_reverify_same_round_implies_recoverable() {
991 harness::certify_after_restart_reverify_same_round_implies_recoverable::<CodingHarness>();
992 }
993
994 #[test_traced("WARN")]
995 fn test_coding_certify_at_later_view_survives_earlier_view_pruning() {
996 harness::certify_at_later_view_survives_earlier_view_pruning::<CodingHarness>();
997 }
998
999 #[test_traced("WARN")]
1000 fn test_coding_certify_first_block_fetches_genesis_parent() {
1001 let runner = deterministic::Runner::timed(Duration::from_secs(60));
1002 runner.start(|mut context| async move {
1003 let Fixture {
1004 participants,
1005 schemes,
1006 ..
1007 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1008 let mut oracle = setup_network_with_participants(
1009 context.child("network"),
1010 NZUsize!(1),
1011 participants.clone(),
1012 )
1013 .await;
1014
1015 let me = participants[0].clone();
1016 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1017
1018 let setup = CodingHarness::setup_validator(
1019 context.child("validator").with_attribute("index", 0),
1020 &mut oracle,
1021 me.clone(),
1022 ConstantProvider::new(schemes[0].clone()),
1023 )
1024 .await;
1025 let marshal = setup.mailbox;
1026 let shards = setup.extra;
1027
1028 let genesis_ctx = CodingCtx {
1029 round: Round::zero(),
1030 leader: default_leader(),
1031 parent: (View::zero(), genesis_commitment()),
1032 };
1033 let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
1034 let genesis_parent_commitment = genesis_coding_commitment::<Sha256, _>(&genesis);
1035
1036 let round = Round::new(Epoch::zero(), View::new(1));
1037 let block_ctx = CodingCtx {
1038 round,
1039 leader: me.clone(),
1040 parent: (View::zero(), genesis_parent_commitment),
1041 };
1042 let block = make_coding_block(block_ctx.clone(), genesis.digest(), Height::new(1), 100);
1043 let coded_block = CodedBlock::new(block, coding_config, &Sequential);
1044 let commitment = coded_block.commitment();
1045 shards.proposed(round, coded_block);
1046
1047 context.sleep(Duration::from_millis(10)).await;
1048
1049 let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
1050 let cfg = MarshaledConfig {
1051 application: mock_app,
1052 marshal,
1053 shards,
1054 scheme_provider: ConstantProvider::new(schemes[0].clone()),
1055 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
1056 strategy: Sequential,
1057 };
1058 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
1059
1060 let shard_validity = marshaled
1061 .verify(block_ctx, commitment)
1062 .await
1063 .await
1064 .expect("verify result missing");
1065 assert!(shard_validity, "shard validity should pass");
1066
1067 let certify_result = marshaled
1068 .certify(round, commitment)
1069 .await
1070 .await
1071 .expect("certify result missing");
1072 assert!(
1073 certify_result,
1074 "height-1 block should certify with genesis as parent"
1075 );
1076 });
1077 }
1078
1079 #[test_traced("WARN")]
1086 fn test_coding_store_finalization_does_not_prune_buffer_before_repair() {
1087 let runner = deterministic::Runner::timed(Duration::from_secs(60));
1088 runner.start(|mut context| async move {
1089 let Fixture {
1090 participants,
1091 schemes,
1092 ..
1093 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1094 let mut oracle = setup_network_with_participants(
1095 context.child("network"),
1096 NZUsize!(1),
1097 participants.clone(),
1098 )
1099 .await;
1100
1101 let setup = CodingHarness::setup_validator(
1102 context.child("validator").with_attribute("index", 0),
1103 &mut oracle,
1104 participants[0].clone(),
1105 ConstantProvider::new(schemes[0].clone()),
1106 )
1107 .await;
1108 let mut handle = harness::ValidatorHandle::<CodingHarness> {
1109 mailbox: setup.mailbox,
1110 extra: setup.extra,
1111 };
1112
1113 let parent_block = CodingHarness::make_test_block(
1115 Sha256::hash(b""),
1116 CodingHarness::genesis_parent_commitment(NUM_VALIDATORS as u16),
1117 Height::new(1),
1118 1,
1119 NUM_VALIDATORS as u16,
1120 );
1121 let parent_digest = CodingHarness::digest(&parent_block);
1122 let parent_commitment = CodingHarness::commitment(&parent_block);
1123
1124 let descendant_block = CodingHarness::make_test_block(
1125 parent_digest,
1126 parent_commitment,
1127 Height::new(2),
1128 2,
1129 NUM_VALIDATORS as u16,
1130 );
1131 let descendant_commitment = CodingHarness::commitment(&descendant_block);
1132
1133 CodingHarness::propose(
1135 &mut handle,
1136 Round::new(Epoch::new(0), View::new(1)),
1137 &parent_block,
1138 )
1139 .await;
1140 CodingHarness::propose(
1141 &mut handle,
1142 Round::new(Epoch::new(0), View::new(2)),
1143 &descendant_block,
1144 )
1145 .await;
1146
1147 let descendant_proposal = Proposal {
1152 round: Round::new(Epoch::new(0), View::new(2)),
1153 parent: View::new(1),
1154 payload: descendant_commitment,
1155 };
1156 let descendant_finalization =
1157 CodingHarness::make_finalization(descendant_proposal, &schemes, QUORUM);
1158 CodingHarness::report_finalization(&mut handle.mailbox, descendant_finalization).await;
1159
1160 while handle.mailbox.get_block(Height::new(2)).await.is_none() {
1164 context.sleep(Duration::from_millis(10)).await;
1165 }
1166
1167 let parent = handle.mailbox.get_block(Height::new(1)).await;
1168 assert!(
1169 parent.is_some(),
1170 "parent must be archived from shard buffer before height-prune evicts it"
1171 );
1172 });
1173 }
1174
1175 #[test_traced("WARN")]
1176 fn test_coding_init_processed_height() {
1177 harness::init_processed_height::<CodingHarness>();
1178 }
1179
1180 #[test_traced("INFO")]
1181 fn test_coding_broadcast_caches_block() {
1182 harness::broadcast_caches_block::<CodingHarness>();
1183 }
1184
1185 #[test_traced("INFO")]
1190 fn test_certify_lower_view_after_higher_view() {
1191 let runner = deterministic::Runner::timed(Duration::from_secs(60));
1192 runner.start(|mut context| async move {
1193 let Fixture {
1194 participants,
1195 schemes,
1196 ..
1197 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1198 let mut oracle = setup_network_with_participants(
1199 context.child("network"),
1200 NZUsize!(1),
1201 participants.clone(),
1202 )
1203 .await;
1204
1205 let me = participants[0].clone();
1206 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1207
1208 let setup = CodingHarness::setup_validator(
1209 context.child("validator").with_attribute("index", 0),
1210 &mut oracle,
1211 me.clone(),
1212 ConstantProvider::new(schemes[0].clone()),
1213 )
1214 .await;
1215 let marshal = setup.mailbox;
1216 let shards = setup.extra;
1217
1218 let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
1219
1220 let cfg = MarshaledConfig {
1221 application: mock_app,
1222 marshal: marshal.clone(),
1223 shards: shards.clone(),
1224 scheme_provider: ConstantProvider::new(schemes[0].clone()),
1225 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
1226 strategy: Sequential,
1227 };
1228 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
1229
1230 let genesis_ctx = CodingCtx {
1231 round: Round::zero(),
1232 leader: default_leader(),
1233 parent: (View::zero(), genesis_commitment()),
1234 };
1235 let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
1236
1237 let parent_ctx = CodingCtx {
1239 round: Round::new(Epoch::new(0), View::new(1)),
1240 leader: default_leader(),
1241 parent: (View::zero(), genesis_commitment()),
1242 };
1243 let parent = make_coding_block(parent_ctx, genesis.digest(), Height::new(1), 100);
1244 let parent_digest = parent.digest();
1245 let coded_parent = CodedBlock::new(parent.clone(), coding_config, &Sequential);
1246 let parent_commitment = coded_parent.commitment();
1247 shards.proposed(Round::new(Epoch::new(0), View::new(1)), coded_parent);
1248
1249 let round_a = Round::new(Epoch::new(0), View::new(5));
1251 let context_a = CodingCtx {
1252 round: round_a,
1253 leader: me.clone(),
1254 parent: (View::new(1), parent_commitment),
1255 };
1256 let block_a = make_coding_block(context_a.clone(), parent_digest, Height::new(2), 200);
1257 let coded_block_a = CodedBlock::new(block_a.clone(), coding_config, &Sequential);
1258 let commitment_a = coded_block_a.commitment();
1259 shards.proposed(round_a, coded_block_a);
1260
1261 let round_b = Round::new(Epoch::new(0), View::new(10));
1264 let context_b = CodingCtx {
1265 round: round_b,
1266 leader: me.clone(),
1267 parent: (View::new(1), parent_commitment),
1268 };
1269 let block_b = make_coding_block(context_b.clone(), parent_digest, Height::new(2), 300);
1270 let coded_block_b = CodedBlock::new(block_b.clone(), coding_config, &Sequential);
1271 let commitment_b = coded_block_b.commitment();
1272 shards.proposed(round_b, coded_block_b);
1273
1274 context.sleep(Duration::from_millis(10)).await;
1275
1276 let _ = marshaled.verify(context_a, commitment_a).await.await;
1278
1279 let _ = marshaled.verify(context_b, commitment_b).await.await;
1281
1282 let certify_b = marshaled.certify(round_b, commitment_b).await;
1284 assert!(
1285 certify_b.await.unwrap(),
1286 "Block B certification should succeed"
1287 );
1288
1289 let certify_a = marshaled.certify(round_a, commitment_a).await;
1291
1292 select! {
1294 result = certify_a => {
1295 assert!(result.unwrap(), "Block A certification should succeed");
1296 },
1297 _ = context.sleep(Duration::from_secs(5)) => {
1298 panic!("Block A certification timed out");
1299 },
1300 }
1301 })
1302 }
1303
1304 #[test_traced("INFO")]
1313 fn test_marshaled_reproposal_validation() {
1314 let runner = deterministic::Runner::timed(Duration::from_secs(60));
1315 runner.start(|mut context| async move {
1316 let Fixture {
1317 participants,
1318 schemes,
1319 ..
1320 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1321 let mut oracle = setup_network_with_participants(
1322 context.child("network"),
1323 NZUsize!(1),
1324 participants.clone(),
1325 )
1326 .await;
1327
1328 let me = participants[0].clone();
1329 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1330
1331 let setup = CodingHarness::setup_validator(
1332 context.child("validator").with_attribute("index", 0),
1333 &mut oracle,
1334 me.clone(),
1335 ConstantProvider::new(schemes[0].clone()),
1336 )
1337 .await;
1338 let marshal = setup.mailbox;
1339 let shards = setup.extra;
1340
1341 let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
1342 let cfg = MarshaledConfig {
1343 application: mock_app,
1344 marshal: marshal.clone(),
1345 shards: shards.clone(),
1346 scheme_provider: ConstantProvider::new(schemes[0].clone()),
1347 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
1348 strategy: Sequential,
1349 };
1350 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
1351
1352 let genesis_ctx = CodingCtx {
1353 round: Round::zero(),
1354 leader: default_leader(),
1355 parent: (View::zero(), genesis_commitment()),
1356 };
1357 let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
1358
1359 let mut parent = genesis.digest();
1362 let mut last_view = View::zero();
1363 let mut last_commitment = genesis_commitment();
1364 for i in 1..BLOCKS_PER_EPOCH.get() {
1365 let round = Round::new(Epoch::new(0), View::new(i));
1366 let ctx = CodingCtx {
1367 round,
1368 leader: me.clone(),
1369 parent: (last_view, last_commitment),
1370 };
1371 let block = make_coding_block(ctx.clone(), parent, Height::new(i), i * 100);
1372 let coded_block = CodedBlock::new(block.clone(), coding_config, &Sequential);
1373 last_commitment = coded_block.commitment();
1374 shards.proposed(round, coded_block);
1375 parent = block.digest();
1376 last_view = View::new(i);
1377 }
1378
1379 let boundary_height = Height::new(BLOCKS_PER_EPOCH.get() - 1);
1381 let boundary_round = Round::new(Epoch::new(0), View::new(boundary_height.get()));
1382 let boundary_context = CodingCtx {
1383 round: boundary_round,
1384 leader: me.clone(),
1385 parent: (last_view, last_commitment),
1386 };
1387 let boundary_block = make_coding_block(
1388 boundary_context.clone(),
1389 parent,
1390 boundary_height,
1391 boundary_height.get() * 100,
1392 );
1393 let coded_boundary =
1394 CodedBlock::new(boundary_block.clone(), coding_config, &Sequential);
1395 let boundary_commitment = coded_boundary.commitment();
1396 shards.proposed(boundary_round, coded_boundary);
1397
1398 context.sleep(Duration::from_millis(10)).await;
1399
1400 let reproposal_round = Round::new(Epoch::new(0), View::new(20));
1408 let reproposal_context = CodingCtx {
1409 round: reproposal_round,
1410 leader: me.clone(),
1411 parent: (View::new(boundary_height.get()), boundary_commitment), };
1413
1414 let shard_validity = marshaled
1418 .verify(reproposal_context.clone(), boundary_commitment)
1419 .await
1420 .await;
1421 assert!(
1422 shard_validity.unwrap(),
1423 "Re-proposal verify should return true for shard validity"
1424 );
1425
1426 let certify_result = marshaled
1428 .certify(reproposal_round, boundary_commitment)
1429 .await
1430 .await;
1431 assert!(
1432 certify_result.unwrap(),
1433 "Valid re-proposal at epoch boundary should be accepted"
1434 );
1435
1436 let non_boundary_height = Height::new(10);
1439 let non_boundary_round = Round::new(Epoch::new(0), View::new(10));
1440 let non_boundary_context = CodingCtx {
1442 round: non_boundary_round,
1443 leader: me.clone(),
1444 parent: (View::new(9), last_commitment), };
1446 let non_boundary_block = make_coding_block(
1447 non_boundary_context.clone(),
1448 parent,
1449 non_boundary_height,
1450 1000,
1451 );
1452 let coded_non_boundary =
1453 CodedBlock::new(non_boundary_block.clone(), coding_config, &Sequential);
1454 let non_boundary_commitment = coded_non_boundary.commitment();
1455
1456 shards.proposed(non_boundary_round, coded_non_boundary);
1458
1459 context.sleep(Duration::from_millis(10)).await;
1460
1461 let invalid_reproposal_round = Round::new(Epoch::new(0), View::new(15));
1463 let invalid_reproposal_context = CodingCtx {
1464 round: invalid_reproposal_round,
1465 leader: me.clone(),
1466 parent: (View::new(10), non_boundary_commitment),
1467 };
1468
1469 let shard_validity = marshaled
1473 .verify(invalid_reproposal_context, non_boundary_commitment)
1474 .await
1475 .await;
1476 assert!(
1477 !shard_validity.unwrap(),
1478 "Invalid re-proposal verify should return false"
1479 );
1480
1481 let certify_result = marshaled
1483 .certify(invalid_reproposal_round, non_boundary_commitment)
1484 .await
1485 .await;
1486 assert!(
1487 !certify_result.unwrap(),
1488 "Invalid re-proposal (not at epoch boundary) should be rejected"
1489 );
1490
1491 let cross_epoch_reproposal_round = Round::new(Epoch::new(1), View::new(20));
1494 let cross_epoch_reproposal_context = CodingCtx {
1495 round: cross_epoch_reproposal_round,
1496 leader: me.clone(),
1497 parent: (View::new(boundary_height.get()), boundary_commitment),
1498 };
1499
1500 let shard_validity = marshaled
1504 .verify(cross_epoch_reproposal_context.clone(), boundary_commitment)
1505 .await
1506 .await;
1507 assert!(
1508 !shard_validity.unwrap(),
1509 "Cross-epoch re-proposal verify should return false"
1510 );
1511
1512 let certify_result = marshaled
1514 .certify(cross_epoch_reproposal_round, boundary_commitment)
1515 .await
1516 .await;
1517 assert!(
1518 !certify_result.unwrap(),
1519 "Re-proposal with mismatched epoch should be rejected"
1520 );
1521
1522 })
1527 }
1528
1529 #[test_traced("WARN")]
1530 fn test_marshaled_rejects_mismatched_context_digest() {
1531 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1532 runner.start(|mut context| async move {
1533 let Fixture {
1534 participants,
1535 schemes,
1536 ..
1537 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1538 let mut oracle = setup_network_with_participants(
1539 context.child("network"),
1540 NZUsize!(1),
1541 participants.clone(),
1542 )
1543 .await;
1544
1545 let me = participants[0].clone();
1546 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1547
1548 let setup = CodingHarness::setup_validator(
1549 context.child("validator").with_attribute("index", 0),
1550 &mut oracle,
1551 me.clone(),
1552 ConstantProvider::new(schemes[0].clone()),
1553 )
1554 .await;
1555 let marshal = setup.mailbox;
1556 let shards = setup.extra;
1557
1558 let genesis_ctx = CodingCtx {
1559 round: Round::zero(),
1560 leader: default_leader(),
1561 parent: (View::zero(), genesis_commitment()),
1562 };
1563 let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
1564
1565 let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
1566 let cfg = MarshaledConfig {
1567 application: mock_app,
1568 marshal: marshal.clone(),
1569 shards: shards.clone(),
1570 scheme_provider: ConstantProvider::new(schemes[0].clone()),
1571 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
1572 strategy: Sequential,
1573 };
1574 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
1575
1576 let parent_ctx = CodingCtx {
1578 round: Round::new(Epoch::zero(), View::new(1)),
1579 leader: default_leader(),
1580 parent: (View::zero(), genesis_commitment()),
1581 };
1582 let parent = make_coding_block(parent_ctx, genesis.digest(), Height::new(1), 100);
1583 let coded_parent = CodedBlock::new(parent.clone(), coding_config, &Sequential);
1584 let parent_commitment = coded_parent.commitment();
1585 shards.proposed(Round::new(Epoch::zero(), View::new(1)), coded_parent);
1586
1587 let round_a = Round::new(Epoch::zero(), View::new(2));
1589 let context_a = CodingCtx {
1590 round: round_a,
1591 leader: me.clone(),
1592 parent: (View::new(1), parent_commitment),
1593 };
1594 let block_a = make_coding_block(context_a, parent.digest(), Height::new(2), 200);
1595 let coded_block_a: CodedBlock<_, ReedSolomon<Sha256>, Sha256> =
1596 CodedBlock::new(block_a, coding_config, &Sequential);
1597 let commitment_a = coded_block_a.commitment();
1598
1599 let round_b = Round::new(Epoch::zero(), View::new(3));
1601 let context_b = CodingCtx {
1602 round: round_b,
1603 leader: participants[1].clone(),
1604 parent: (View::new(1), parent_commitment),
1605 };
1606
1607 let verify_rx = marshaled.verify(context_b, commitment_a).await;
1608 select! {
1609 result = verify_rx => {
1610 assert!(
1611 !result.unwrap(),
1612 "mismatched context digest should be rejected"
1613 );
1614 },
1615 _ = context.sleep(Duration::from_secs(5)) => {
1616 panic!("verify should reject mismatched context digest promptly");
1617 },
1618 }
1619 })
1620 }
1621
1622 #[test_traced("WARN")]
1623 fn test_reproposal_certify_recovers_after_verify_receiver_drop() {
1624 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1625 runner.start(|mut context| async move {
1626 let Fixture {
1627 participants,
1628 schemes,
1629 ..
1630 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1631 let mut oracle = setup_network_with_participants(
1632 context.child("network"),
1633 NZUsize!(1),
1634 participants.clone(),
1635 )
1636 .await;
1637
1638 let me = participants[0].clone();
1639 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1640
1641 let setup = CodingHarness::setup_validator(
1642 context.child("validator").with_attribute("index", 0),
1643 &mut oracle,
1644 me.clone(),
1645 ConstantProvider::new(schemes[0].clone()),
1646 )
1647 .await;
1648 let marshal = setup.mailbox;
1649 let shards = setup.extra;
1650
1651 let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
1652 let cfg = MarshaledConfig {
1653 application: mock_app,
1654 marshal: marshal.clone(),
1655 shards: shards.clone(),
1656 scheme_provider: ConstantProvider::new(schemes[0].clone()),
1657 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
1658 strategy: Sequential,
1659 };
1660 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
1661
1662 let genesis_ctx = CodingCtx {
1663 round: Round::zero(),
1664 leader: default_leader(),
1665 parent: (View::zero(), genesis_commitment()),
1666 };
1667 let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
1668
1669 let boundary_height = Height::new(BLOCKS_PER_EPOCH.get() - 1);
1672 let boundary_round = Round::new(Epoch::zero(), View::new(boundary_height.get()));
1673 let boundary_context = CodingCtx {
1674 round: boundary_round,
1675 leader: me.clone(),
1676 parent: (View::zero(), genesis_commitment()),
1677 };
1678 let boundary_block = make_coding_block(
1679 boundary_context,
1680 genesis.digest(),
1681 boundary_height,
1682 boundary_height.get() * 100,
1683 );
1684 let coded_boundary = CodedBlock::new(boundary_block, coding_config, &Sequential);
1685 let boundary_commitment = coded_boundary.commitment();
1686 let reproposal_round = Round::new(Epoch::zero(), View::new(boundary_height.get() + 1));
1687 let reproposal_context = CodingCtx {
1688 round: reproposal_round,
1689 leader: me,
1690 parent: (View::new(boundary_height.get()), boundary_commitment),
1691 };
1692
1693 let verify_rx = marshaled
1695 .verify(reproposal_context, boundary_commitment)
1696 .await;
1697 drop(verify_rx);
1698 context.sleep(Duration::from_millis(10)).await;
1699
1700 shards.proposed(boundary_round, coded_boundary);
1701 context.sleep(Duration::from_millis(10)).await;
1702
1703 let certify_rx = marshaled
1706 .certify(reproposal_round, boundary_commitment)
1707 .await;
1708 select! {
1709 result = certify_rx => {
1710 assert!(
1711 result.expect("certify result missing"),
1712 "certify should recover after verify receiver drop"
1713 );
1714 },
1715 _ = context.sleep(Duration::from_secs(5)) => {
1716 panic!("certify should recover after verify receiver drop");
1717 },
1718 }
1719 })
1720 }
1721
1722 #[test_traced("WARN")]
1723 fn test_reproposal_missing_block_does_not_synthesize_false() {
1724 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1725 runner.start(|mut context| async move {
1726 let Fixture {
1727 participants,
1728 schemes,
1729 ..
1730 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1731 let mut oracle = setup_network_with_participants(
1732 context.child("network"),
1733 NZUsize!(1),
1734 participants.clone(),
1735 )
1736 .await;
1737
1738 let me = participants[0].clone();
1739 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1740
1741 let setup = CodingHarness::setup_validator(
1742 context.child("validator").with_attribute("index", 0),
1743 &mut oracle,
1744 me.clone(),
1745 ConstantProvider::new(schemes[0].clone()),
1746 )
1747 .await;
1748 let marshal = setup.mailbox;
1749 let shards = setup.extra;
1750
1751 let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
1752 let cfg = MarshaledConfig {
1753 application: mock_app,
1754 marshal: marshal.clone(),
1755 shards: shards.clone(),
1756 scheme_provider: ConstantProvider::new(schemes[0].clone()),
1757 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
1758 strategy: Sequential,
1759 };
1760 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
1761
1762 let missing_payload = Commitment::from((
1764 Sha256::hash(b"missing_block"),
1765 Sha256::hash(b"missing_root"),
1766 Sha256::hash(b"missing_context"),
1767 coding_config,
1768 ));
1769 let round = Round::new(Epoch::zero(), View::new(1));
1770 let reproposal_context = CodingCtx {
1771 round,
1772 leader: me,
1773 parent: (View::zero(), missing_payload),
1774 };
1775
1776 let verify_rx = marshaled.verify(reproposal_context, missing_payload).await;
1778
1779 context.sleep(Duration::from_millis(100)).await;
1782 shards.prune(missing_payload);
1783
1784 select! {
1785 result = verify_rx => {
1786 assert!(
1787 result.is_err(),
1788 "verify should resolve without explicit false when re-proposal block is unavailable"
1789 );
1790 },
1791 _ = context.sleep(Duration::from_secs(5)) => {
1792 panic!("verify should resolve promptly when re-proposal block is unavailable");
1793 },
1794 }
1795
1796 let mut certify_rx = marshaled.certify(round, missing_payload).await;
1800 context.sleep(Duration::from_millis(100)).await;
1801 assert!(
1802 matches!(
1803 certify_rx.try_recv(),
1804 Err(commonware_utils::channel::oneshot::error::TryRecvError::Empty)
1805 ),
1806 "certify should remain pending without explicit false or stale cancellation"
1807 );
1808 drop(certify_rx);
1809 })
1810 }
1811
1812 #[test_traced("WARN")]
1813 fn test_core_subscription_closes_when_coding_buffer_prunes_missing_commitment() {
1814 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1815 runner.start(|mut context| async move {
1816 let Fixture {
1817 participants,
1818 schemes,
1819 ..
1820 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1821 let mut oracle = setup_network_with_participants(
1822 context.child("network"),
1823 NZUsize!(1),
1824 participants.clone(),
1825 )
1826 .await;
1827
1828 let setup = CodingHarness::setup_validator(
1829 context.child("validator").with_attribute("index", 0),
1830 &mut oracle,
1831 participants[0].clone(),
1832 ConstantProvider::new(schemes[0].clone()),
1833 )
1834 .await;
1835 let marshal = setup.mailbox;
1836 let shards = setup.extra;
1837
1838 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1839 let missing_commitment = Commitment::from((
1840 Sha256::hash(b"missing_block"),
1841 Sha256::hash(b"missing_root"),
1842 Sha256::hash(b"missing_context"),
1843 coding_config,
1844 ));
1845 let round = Round::new(Epoch::zero(), View::new(1));
1846
1847 let block_rx = marshal.subscribe_by_commitment(
1850 missing_commitment,
1851 core::CommitmentFallback::FetchByRound { round },
1852 );
1853
1854 context.sleep(Duration::from_millis(100)).await;
1856
1857 shards.prune(missing_commitment);
1860
1861 select! {
1864 result = block_rx => {
1865 assert!(
1866 result.is_err(),
1867 "core subscription should close when coding buffer drops subscription"
1868 );
1869 },
1870 _ = context.sleep(Duration::from_secs(5)) => {
1871 panic!("core subscription should resolve promptly after coding prune");
1872 },
1873 }
1874 })
1875 }
1876
1877 #[test_traced("WARN")]
1878 fn test_marshaled_rejects_unsupported_epoch() {
1879 #[derive(Clone)]
1880 struct LimitedEpocher {
1881 inner: FixedEpocher,
1882 max_epoch: u64,
1883 }
1884
1885 impl Epocher for LimitedEpocher {
1886 fn containing(&self, height: Height) -> Option<crate::types::EpochInfo> {
1887 let bounds = self.inner.containing(height)?;
1888 if bounds.epoch().get() > self.max_epoch {
1889 None
1890 } else {
1891 Some(bounds)
1892 }
1893 }
1894
1895 fn first(&self, epoch: Epoch) -> Option<Height> {
1896 if epoch.get() > self.max_epoch {
1897 None
1898 } else {
1899 self.inner.first(epoch)
1900 }
1901 }
1902
1903 fn last(&self, epoch: Epoch) -> Option<Height> {
1904 if epoch.get() > self.max_epoch {
1905 None
1906 } else {
1907 self.inner.last(epoch)
1908 }
1909 }
1910 }
1911
1912 let runner = deterministic::Runner::timed(Duration::from_secs(60));
1913 runner.start(|mut context| async move {
1914 let Fixture {
1915 participants,
1916 schemes,
1917 ..
1918 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1919 let mut oracle = setup_network_with_participants(
1920 context.child("network"),
1921 NZUsize!(1),
1922 participants.clone(),
1923 )
1924 .await;
1925
1926 let me = participants[0].clone();
1927 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1928
1929 let setup = CodingHarness::setup_validator(
1930 context.child("validator").with_attribute("index", 0),
1931 &mut oracle,
1932 me.clone(),
1933 ConstantProvider::new(schemes[0].clone()),
1934 )
1935 .await;
1936 let marshal = setup.mailbox;
1937 let shards = setup.extra;
1938
1939 let genesis_ctx = CodingCtx {
1940 round: Round::zero(),
1941 leader: default_leader(),
1942 parent: (View::zero(), genesis_commitment()),
1943 };
1944 let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
1945
1946 let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
1947 let limited_epocher = LimitedEpocher {
1948 inner: FixedEpocher::new(BLOCKS_PER_EPOCH),
1949 max_epoch: 0,
1950 };
1951 let cfg = MarshaledConfig {
1952 application: mock_app,
1953 marshal: marshal.clone(),
1954 shards: shards.clone(),
1955 scheme_provider: ConstantProvider::new(schemes[0].clone()),
1956 epocher: limited_epocher,
1957 strategy: Sequential,
1958 };
1959 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
1960
1961 let parent_ctx = CodingCtx {
1963 round: Round::new(Epoch::zero(), View::new(19)),
1964 leader: default_leader(),
1965 parent: (View::zero(), genesis_commitment()),
1966 };
1967 let parent = make_coding_block(parent_ctx, genesis.digest(), Height::new(19), 1000);
1968 let parent_digest = parent.digest();
1969 let coded_parent = CodedBlock::new(parent.clone(), coding_config, &Sequential);
1970 let parent_commitment = coded_parent.commitment();
1971 shards.proposed(Round::new(Epoch::zero(), View::new(19)), coded_parent);
1972
1973 let block_ctx = CodingCtx {
1975 round: Round::new(Epoch::new(1), View::new(20)),
1976 leader: default_leader(),
1977 parent: (View::new(19), parent_commitment),
1978 };
1979 let block = make_coding_block(block_ctx, parent_digest, Height::new(20), 2000);
1980 let coded_block = CodedBlock::new(block.clone(), coding_config, &Sequential);
1981 let block_commitment = coded_block.commitment();
1982 shards.proposed(Round::new(Epoch::new(1), View::new(20)), coded_block);
1983
1984 context.sleep(Duration::from_millis(10)).await;
1985
1986 let unsupported_round = Round::new(Epoch::new(1), View::new(20));
1989 let unsupported_context = CodingCtx {
1990 round: unsupported_round,
1991 leader: me.clone(),
1992 parent: (View::new(19), parent_commitment),
1993 };
1994
1995 let _shard_validity = marshaled
1997 .verify(unsupported_context, block_commitment)
1998 .await;
1999
2000 let certify_result = marshaled
2002 .certify(unsupported_round, block_commitment)
2003 .await
2004 .await;
2005
2006 assert!(
2007 !certify_result.unwrap(),
2008 "Block in unsupported epoch should be rejected"
2009 );
2010 })
2011 }
2012
2013 #[test_traced("WARN")]
2014 fn test_marshaled_rejects_invalid_ancestry() {
2015 let runner = deterministic::Runner::timed(Duration::from_secs(60));
2016 runner.start(|mut context| async move {
2017 let Fixture {
2018 participants,
2019 schemes,
2020 ..
2021 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2022 let mut oracle = setup_network_with_participants(
2023 context.child("network"),
2024 NZUsize!(1),
2025 participants.clone(),
2026 )
2027 .await;
2028
2029 let me = participants[0].clone();
2030 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2031
2032 let setup = CodingHarness::setup_validator(
2033 context.child("validator").with_attribute("index", 0),
2034 &mut oracle,
2035 me.clone(),
2036 ConstantProvider::new(schemes[0].clone()),
2037 )
2038 .await;
2039 let marshal = setup.mailbox;
2040 let shards = setup.extra;
2041
2042 let genesis_ctx = CodingCtx {
2044 round: Round::zero(),
2045 leader: default_leader(),
2046 parent: (View::zero(), genesis_commitment()),
2047 };
2048 let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
2049
2050 let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
2052 let cfg = MarshaledConfig {
2053 application: mock_app,
2054 marshal: marshal.clone(),
2055 shards: shards.clone(),
2056 scheme_provider: ConstantProvider::new(schemes[0].clone()),
2057 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
2058 strategy: Sequential,
2059 };
2060 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
2061
2062 let honest_parent_ctx = CodingCtx {
2069 round: Round::new(Epoch::new(1), View::new(21)),
2070 leader: default_leader(),
2071 parent: (View::zero(), genesis_commitment()),
2072 };
2073 let honest_parent = make_coding_block(
2074 honest_parent_ctx,
2075 genesis.digest(),
2076 Height::new(BLOCKS_PER_EPOCH.get() + 1),
2077 1000,
2078 );
2079 let parent_digest = honest_parent.digest();
2080 let coded_parent = CodedBlock::new(honest_parent.clone(), coding_config, &Sequential);
2081 let parent_commitment = coded_parent.commitment();
2082 shards.proposed(Round::new(Epoch::new(1), View::new(21)), coded_parent);
2083
2084 let byzantine_round = Round::new(Epoch::new(1), View::new(35));
2088 let byzantine_context = CodingCtx {
2089 round: byzantine_round,
2090 leader: me.clone(),
2091 parent: (View::new(21), parent_commitment), };
2093 let malicious_block = make_coding_block(
2094 byzantine_context.clone(),
2095 parent_digest,
2096 Height::new(BLOCKS_PER_EPOCH.get() + 15), 2000,
2098 );
2099 let coded_malicious =
2100 CodedBlock::new(malicious_block.clone(), coding_config, &Sequential);
2101 let malicious_commitment = coded_malicious.commitment();
2102 shards.proposed(byzantine_round, coded_malicious);
2103
2104 context.sleep(Duration::from_millis(10)).await;
2106
2107 let _shard_validity = marshaled
2114 .verify(byzantine_context, malicious_commitment)
2115 .await;
2116
2117 let certify_result = marshaled
2119 .certify(byzantine_round, malicious_commitment)
2120 .await
2121 .await;
2122
2123 assert!(
2124 !certify_result.unwrap(),
2125 "Byzantine block with non-contiguous heights should be rejected"
2126 );
2127
2128 let byzantine_round2 = Round::new(Epoch::new(1), View::new(22));
2133 let byzantine_context2 = CodingCtx {
2134 round: byzantine_round2,
2135 leader: me.clone(),
2136 parent: (View::new(21), parent_commitment), };
2138 let malicious_block2 = make_coding_block(
2139 byzantine_context2.clone(),
2140 genesis.digest(), Height::new(BLOCKS_PER_EPOCH.get() + 2),
2142 3000,
2143 );
2144 let coded_malicious2 =
2145 CodedBlock::new(malicious_block2.clone(), coding_config, &Sequential);
2146 let malicious_commitment2 = coded_malicious2.commitment();
2147 shards.proposed(byzantine_round2, coded_malicious2);
2148
2149 context.sleep(Duration::from_millis(10)).await;
2151
2152 let _shard_validity = marshaled
2160 .verify(byzantine_context2, malicious_commitment2)
2161 .await;
2162
2163 let certify_result = marshaled
2165 .certify(byzantine_round2, malicious_commitment2)
2166 .await
2167 .await;
2168
2169 assert!(
2170 !certify_result.unwrap(),
2171 "Byzantine block with mismatched parent commitment should be rejected"
2172 );
2173 })
2174 }
2175
2176 #[test_traced("WARN")]
2177 fn test_certify_without_prior_verify_crash_recovery() {
2178 let runner = deterministic::Runner::timed(Duration::from_secs(30));
2185 runner.start(|mut context| async move {
2186 let Fixture {
2187 participants,
2188 schemes,
2189 ..
2190 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2191 let mut oracle = setup_network_with_participants(
2192 context.child("network"),
2193 NZUsize!(1),
2194 participants.clone(),
2195 )
2196 .await;
2197
2198 let me = participants[0].clone();
2199 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2200
2201 let setup = CodingHarness::setup_validator(
2202 context.child("validator").with_attribute("index", 0),
2203 &mut oracle,
2204 me.clone(),
2205 ConstantProvider::new(schemes[0].clone()),
2206 )
2207 .await;
2208 let marshal = setup.mailbox;
2209 let shards = setup.extra;
2210
2211 let genesis_ctx = CodingCtx {
2212 round: Round::zero(),
2213 leader: default_leader(),
2214 parent: (View::zero(), genesis_commitment()),
2215 };
2216 let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
2217
2218 let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
2219 let cfg = MarshaledConfig {
2220 application: mock_app,
2221 marshal: marshal.clone(),
2222 shards: shards.clone(),
2223 scheme_provider: ConstantProvider::new(schemes[0].clone()),
2224 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
2225 strategy: Sequential,
2226 };
2227 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
2228
2229 let parent_round = Round::new(Epoch::zero(), View::new(1));
2231 let parent_ctx = CodingCtx {
2232 round: parent_round,
2233 leader: default_leader(),
2234 parent: (View::zero(), genesis_commitment()),
2235 };
2236 let parent = make_coding_block(parent_ctx, genesis.digest(), Height::new(1), 100);
2237 let coded_parent = CodedBlock::new(parent.clone(), coding_config, &Sequential);
2238 let parent_commitment = coded_parent.commitment();
2239 shards.proposed(parent_round, coded_parent);
2240
2241 let child_round = Round::new(Epoch::zero(), View::new(2));
2243 let child_ctx = CodingCtx {
2244 round: child_round,
2245 leader: me.clone(),
2246 parent: (View::new(1), parent_commitment),
2247 };
2248 let child = make_coding_block(child_ctx, parent.digest(), Height::new(2), 200);
2249 let coded_child = CodedBlock::new(child, coding_config, &Sequential);
2250 let child_commitment = coded_child.commitment();
2251 shards.proposed(child_round, coded_child);
2252
2253 context.sleep(Duration::from_millis(10)).await;
2254
2255 let certify_rx = marshaled.certify(child_round, child_commitment).await;
2257 select! {
2258 result = certify_rx => {
2259 assert!(
2260 result.unwrap(),
2261 "certify without prior verify should succeed for valid block"
2262 );
2263 },
2264 _ = context.sleep(Duration::from_secs(5)) => {
2265 panic!("certify should complete within timeout");
2266 },
2267 }
2268 })
2269 }
2270
2271 #[test_traced("WARN")]
2277 fn test_malformed_commitment_config_rejected_at_deserialization() {
2278 use commonware_codec::{Encode, ReadExt};
2279
2280 let malformed_bytes = [0u8; Commitment::SIZE];
2284 let result = Commitment::read(&mut &malformed_bytes[..]);
2285 assert!(
2286 result.is_err(),
2287 "deserialization of Commitment with zeroed CodingConfig must fail"
2288 );
2289
2290 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2292 let valid = Commitment::from((
2293 Sha256::hash(b"block"),
2294 Sha256::hash(b"root"),
2295 Sha256::hash(b"context"),
2296 coding_config,
2297 ));
2298 let encoded = valid.encode();
2299 let decoded =
2300 Commitment::read(&mut &encoded[..]).expect("valid Commitment must deserialize");
2301 assert_eq!(valid, decoded);
2302 }
2303
2304 #[test_traced("WARN")]
2305 fn test_certify_propagates_application_verify_failure() {
2306 let runner = deterministic::Runner::timed(Duration::from_secs(30));
2307 runner.start(|mut context| async move {
2308 let Fixture {
2310 participants,
2311 schemes,
2312 ..
2313 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2314 let mut oracle = setup_network_with_participants(
2315 context.child("network"),
2316 NZUsize!(1),
2317 participants.clone(),
2318 )
2319 .await;
2320
2321 let me = participants[0].clone();
2322 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2323
2324 let setup = CodingHarness::setup_validator(
2325 context.child("validator").with_attribute("index", 0),
2326 &mut oracle,
2327 me.clone(),
2328 ConstantProvider::new(schemes[0].clone()),
2329 )
2330 .await;
2331 let marshal = setup.mailbox;
2332 let shards = setup.extra;
2333
2334 let genesis_ctx = CodingCtx {
2335 round: Round::zero(),
2336 leader: default_leader(),
2337 parent: (View::zero(), genesis_commitment()),
2338 };
2339 let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
2340 let mock_app: MockVerifyingApp<CodingB, S> =
2342 MockVerifyingApp::with_verify_result(false);
2343
2344 let cfg = MarshaledConfig {
2345 application: mock_app,
2346 marshal: marshal.clone(),
2347 shards: shards.clone(),
2348 scheme_provider: ConstantProvider::new(schemes[0].clone()),
2349 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
2350 strategy: Sequential,
2351 };
2352 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
2353
2354 let parent_round = Round::new(Epoch::zero(), View::new(1));
2355 let parent_context = CodingCtx {
2356 round: parent_round,
2357 leader: me.clone(),
2358 parent: (View::zero(), genesis_commitment()),
2359 };
2360 let parent = make_coding_block(parent_context, genesis.digest(), Height::new(1), 100);
2361 let coded_parent = CodedBlock::new(parent.clone(), coding_config, &Sequential);
2362 let parent_commitment = coded_parent.commitment();
2363 shards.proposed(parent_round, coded_parent);
2364
2365 let round = Round::new(Epoch::zero(), View::new(2));
2367 let verify_context = CodingCtx {
2368 round,
2369 leader: me,
2370 parent: (View::new(1), parent_commitment),
2371 };
2372 let block =
2373 make_coding_block(verify_context.clone(), parent.digest(), Height::new(2), 200);
2374 let coded_block = CodedBlock::new(block, coding_config, &Sequential);
2375 let commitment = coded_block.commitment();
2376 shards.proposed(round, coded_block);
2377
2378 context.sleep(Duration::from_millis(10)).await;
2379
2380 let optimistic = marshaled.verify(verify_context, commitment).await;
2381 assert!(
2382 optimistic.await.expect("verify result missing"),
2383 "optimistic verify should pass pre-checks and schedule deferred verification"
2384 );
2385
2386 let certify = marshaled.certify(round, commitment).await;
2388 assert!(
2389 !certify.await.expect("certify result missing"),
2390 "certify should propagate deferred application verify failure"
2391 );
2392 })
2393 }
2394
2395 #[test_traced("WARN")]
2396 fn test_backfill_block_mismatched_commitment() {
2397 let runner = deterministic::Runner::timed(Duration::from_secs(30));
2402 runner.start(|mut context| async move {
2403 let Fixture {
2404 participants,
2405 schemes,
2406 ..
2407 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2408 let mut oracle = setup_network_with_participants(
2409 context.child("network"),
2410 NZUsize!(1),
2411 participants[..2].iter().cloned(),
2412 )
2413 .await;
2414
2415 let coding_config_a = coding_config_for_participants(NUM_VALIDATORS as u16);
2416 let coding_config_b = commonware_coding::Config {
2419 minimum_shards: coding_config_a.minimum_shards.checked_add(1).unwrap(),
2420 extra_shards: NZU16!(coding_config_a.extra_shards.get() - 1),
2421 };
2422
2423 let v0_setup = CodingHarness::setup_validator(
2424 context.child("validator").with_attribute("index", 0),
2425 &mut oracle,
2426 participants[0].clone(),
2427 ConstantProvider::new(schemes[0].clone()),
2428 )
2429 .await;
2430 let v1_setup = CodingHarness::setup_validator(
2431 context.child("validator").with_attribute("index", 1),
2432 &mut oracle,
2433 participants[1].clone(),
2434 ConstantProvider::new(schemes[1].clone()),
2435 )
2436 .await;
2437
2438 setup_network_links(&mut oracle, &participants[..2], LINK).await;
2439
2440 let mut v0_mailbox = v0_setup.mailbox;
2441 let v1_mailbox = v1_setup.mailbox;
2442
2443 let genesis_ctx = CodingCtx {
2444 round: Round::zero(),
2445 leader: default_leader(),
2446 parent: (View::zero(), genesis_commitment()),
2447 };
2448 let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
2449
2450 let round1 = Round::new(Epoch::zero(), View::new(1));
2451 let block1_ctx = CodingCtx {
2452 round: round1,
2453 leader: participants[0].clone(),
2454 parent: (View::zero(), genesis_commitment()),
2455 };
2456 let block1 = make_coding_block(block1_ctx, genesis.digest(), Height::new(1), 100);
2457
2458 let coded_block_a: CodedBlock<_, ReedSolomon<Sha256>, Sha256> =
2459 CodedBlock::new(block1.clone(), coding_config_a, &Sequential);
2460 let commitment_a = coded_block_a.commitment();
2461
2462 let coded_block_b: CodedBlock<_, ReedSolomon<Sha256>, Sha256> =
2463 CodedBlock::new(block1.clone(), coding_config_b, &Sequential);
2464 let commitment_b = coded_block_b.commitment();
2465
2466 assert_eq!(coded_block_a.digest(), coded_block_b.digest());
2467 assert_ne!(commitment_a, commitment_b);
2468
2469 assert!(v1_mailbox.verified(round1, coded_block_b.clone()).await);
2472 context.sleep(Duration::from_millis(100)).await;
2473
2474 let proposal: Proposal<Commitment> = Proposal {
2476 round: round1,
2477 parent: View::zero(),
2478 payload: commitment_a,
2479 };
2480 let finalization = CodingHarness::make_finalization(proposal.clone(), &schemes, QUORUM);
2481
2482 CodingHarness::report_finalization(&mut v0_mailbox, finalization).await;
2488
2489 context.sleep(Duration::from_secs(5)).await;
2491
2492 let stored = v0_mailbox.get_block(Height::new(1)).await;
2494 assert!(
2495 stored.is_none(),
2496 "v0 should reject backfilled block with mismatched commitment"
2497 );
2498
2499 let stored_finalization = v0_mailbox.get_finalization(Height::new(1)).await;
2501 assert!(
2502 stored_finalization.is_none(),
2503 "finalization should not be archived until matching block is available"
2504 );
2505 })
2506 }
2507
2508 #[test_traced("WARN")]
2509 #[should_panic(expected = "floor block parent commitment mismatch")]
2510 fn test_coding_floor_anchor_panics_on_parent_commitment_mismatch() {
2511 let runner = deterministic::Runner::timed(Duration::from_secs(30));
2512 runner.start(|mut context| async move {
2513 let Fixture {
2514 participants,
2515 schemes,
2516 ..
2517 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2518 let (mailbox, resolver, _actor_handle) = start_coding_actor_with_recording(
2519 context.child("validator"),
2520 "floor-parent-commitment-mismatch",
2521 ConstantProvider::new(schemes[0].clone()),
2522 RecordingCodingBuffer::default(),
2523 )
2524 .await;
2525
2526 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2527 let parent_round = Round::new(Epoch::zero(), View::new(1));
2528 let parent_context = CodingCtx {
2529 round: parent_round,
2530 leader: participants[0].clone(),
2531 parent: (View::zero(), genesis_commitment()),
2532 };
2533 let parent = make_coding_block(parent_context, Sha256::hash(b""), Height::new(1), 100);
2534
2535 let floor_round = Round::new(Epoch::zero(), View::new(2));
2536 let bad_context = CodingCtx {
2537 round: floor_round,
2538 leader: participants[0].clone(),
2539 parent: (View::new(1), genesis_commitment()),
2540 };
2541 let floor_block = make_coding_block(bad_context, parent.digest(), Height::new(2), 200);
2542 let coded_floor = CodedBlock::new(floor_block, coding_config, &Sequential);
2543 assert_ne!(
2544 coded_floor.parent(),
2545 coded_floor.context().parent.1.block::<D>()
2546 );
2547
2548 let finalization = CodingHarness::make_finalization(
2549 Proposal::new(
2550 floor_round,
2551 View::new(1),
2552 CodingHarness::commitment(&coded_floor),
2553 ),
2554 &schemes,
2555 QUORUM,
2556 );
2557 resolver.respond_to_next_fetch(coded_floor.encode());
2558 mailbox.set_floor(finalization);
2559 context.sleep(Duration::from_secs(5)).await;
2560 })
2561 }
2562
2563 #[test_traced("WARN")]
2567 fn test_marshaled_missing_scheme_skips_propose_and_verify() {
2568 let runner = deterministic::Runner::timed(Duration::from_secs(30));
2569 runner.start(|mut context| async move {
2570 let Fixture {
2571 participants,
2572 schemes,
2573 ..
2574 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2575 let mut oracle = setup_network_with_participants(
2576 context.child("network"),
2577 NZUsize!(1),
2578 participants.clone(),
2579 )
2580 .await;
2581
2582 let me = participants[0].clone();
2583
2584 let setup = CodingHarness::setup_validator(
2585 context.child("validator").with_attribute("index", 0),
2586 &mut oracle,
2587 me.clone(),
2588 ConstantProvider::new(schemes[0].clone()),
2589 )
2590 .await;
2591
2592 let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
2593
2594 let cfg = MarshaledConfig {
2595 application: mock_app,
2596 marshal: setup.mailbox,
2597 shards: setup.extra,
2598 scheme_provider: EmptyProvider,
2599 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
2600 strategy: Sequential,
2601 };
2602 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
2603
2604 let ctx = CodingCtx {
2605 round: Round::new(Epoch::zero(), View::new(1)),
2606 leader: me.clone(),
2607 parent: (View::zero(), genesis_commitment()),
2608 };
2609
2610 let rx = marshaled.propose(ctx.clone()).await;
2612 assert!(rx.await.is_err());
2613
2614 let rx = marshaled.verify(ctx, genesis_commitment()).await;
2616 assert!(rx.await.is_err());
2617 });
2618 }
2619
2620 #[test_traced("WARN")]
2625 fn test_marshaled_certify_persists_block_before_resolving() {
2626 for seed in 0u64..16 {
2627 certify_persists_block_before_resolving_at(seed);
2628 }
2629 }
2630
2631 fn certify_persists_block_before_resolving_at(seed: u64) {
2632 let runner = deterministic::Runner::new(
2633 deterministic::Config::new()
2634 .with_seed(seed)
2635 .with_timeout(Some(Duration::from_secs(60))),
2636 );
2637 runner.start(|mut context| async move {
2638 let Fixture {
2639 participants,
2640 schemes,
2641 ..
2642 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2643 let mut oracle = setup_network_with_participants(
2644 context.child("network"),
2645 NZUsize!(1),
2646 participants.clone(),
2647 )
2648 .await;
2649
2650 let me = participants[0].clone();
2651 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2652
2653 let setup = CodingHarness::setup_validator(
2654 context.child("validator").with_attribute("index", 0),
2655 &mut oracle,
2656 me.clone(),
2657 ConstantProvider::new(schemes[0].clone()),
2658 )
2659 .await;
2660 let marshal = setup.mailbox;
2661 let shards = setup.extra;
2662 let marshal_actor_handle = setup.actor_handle;
2663
2664 let genesis_ctx = CodingCtx {
2665 round: Round::zero(),
2666 leader: default_leader(),
2667 parent: (View::zero(), genesis_commitment()),
2668 };
2669 let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
2670
2671 let parent_round = Round::new(Epoch::zero(), View::new(1));
2674 let parent_ctx = CodingCtx {
2675 round: parent_round,
2676 leader: default_leader(),
2677 parent: (View::zero(), genesis_commitment()),
2678 };
2679 let parent = make_coding_block(parent_ctx, genesis.digest(), Height::new(1), 100);
2680 let coded_parent = CodedBlock::new(parent.clone(), coding_config, &Sequential);
2681 let parent_commitment = coded_parent.commitment();
2682 shards.proposed(parent_round, coded_parent);
2683
2684 let child_round = Round::new(Epoch::zero(), View::new(2));
2685 let child_ctx = CodingCtx {
2686 round: child_round,
2687 leader: me.clone(),
2688 parent: (View::new(1), parent_commitment),
2689 };
2690 let child = make_coding_block(child_ctx.clone(), parent.digest(), Height::new(2), 200);
2691 let coded_child = CodedBlock::new(child.clone(), coding_config, &Sequential);
2692 let child_commitment = coded_child.commitment();
2693 let child_digest = coded_child.digest();
2694 shards.proposed(child_round, coded_child);
2695
2696 context.sleep(Duration::from_millis(10)).await;
2697
2698 let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
2699 let cfg = MarshaledConfig {
2700 application: mock_app,
2701 marshal: marshal.clone(),
2702 shards: shards.clone(),
2703 scheme_provider: ConstantProvider::new(schemes[0].clone()),
2704 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
2705 strategy: Sequential,
2706 };
2707 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
2708
2709 let shard_validity = marshaled
2711 .verify(child_ctx, child_commitment)
2712 .await
2713 .await
2714 .expect("verify result missing");
2715 assert!(shard_validity, "shard validity should pass");
2716
2717 let certify_result = marshaled
2719 .certify(child_round, child_commitment)
2720 .await
2721 .await
2722 .expect("certify result missing");
2723 assert!(certify_result, "certify should succeed");
2724
2725 marshal_actor_handle.abort();
2728 drop(marshaled);
2729 drop(marshal);
2730 drop(shards);
2731
2732 let setup2 = CodingHarness::setup_validator(
2736 context
2737 .child("validator_restart")
2738 .with_attribute("index", 0),
2739 &mut oracle,
2740 me.clone(),
2741 ConstantProvider::new(schemes[0].clone()),
2742 )
2743 .await;
2744 let marshal2 = setup2.mailbox;
2745
2746 let post_restart = marshal2.get_block(&child_digest).await;
2747 assert!(
2748 post_restart.is_some(),
2749 "certify resolved true, so block must be durably persisted"
2750 );
2751 });
2752 }
2753
2754 #[test_traced("WARN")]
2759 fn test_marshaled_proposed_block_persists_across_restart() {
2760 let runner = deterministic::Runner::timed(Duration::from_secs(60));
2761 runner.start(|mut context| async move {
2762 let Fixture {
2763 participants,
2764 schemes,
2765 ..
2766 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2767 let mut oracle = setup_network_with_participants(
2768 context.child("network"),
2769 NZUsize!(1),
2770 participants.clone(),
2771 )
2772 .await;
2773
2774 let me = participants[0].clone();
2775 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2776
2777 let setup = CodingHarness::setup_validator(
2778 context.child("validator").with_attribute("index", 0),
2779 &mut oracle,
2780 me.clone(),
2781 ConstantProvider::new(schemes[0].clone()),
2782 )
2783 .await;
2784 let marshal = setup.mailbox;
2785 let shards = setup.extra;
2786 let marshal_actor_handle = setup.actor_handle;
2787
2788 let genesis_ctx = CodingCtx {
2789 round: Round::zero(),
2790 leader: default_leader(),
2791 parent: (View::zero(), genesis_commitment()),
2792 };
2793 let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
2794 let genesis_parent_commitment = genesis_coding_commitment::<Sha256, _>(&genesis);
2795
2796 let propose_round = Round::new(Epoch::zero(), View::new(1));
2800 let propose_context = CodingCtx {
2801 round: propose_round,
2802 leader: me.clone(),
2803 parent: (View::zero(), genesis_parent_commitment),
2804 };
2805 let block_to_propose = make_coding_block(
2806 propose_context.clone(),
2807 genesis.digest(),
2808 Height::new(1),
2809 100,
2810 );
2811 let block_digest = block_to_propose.digest();
2812 let expected_commitment = CodedBlock::<_, ReedSolomon<Sha256>, Sha256>::new(
2813 block_to_propose.clone(),
2814 coding_config,
2815 &Sequential,
2816 )
2817 .commitment();
2818
2819 let mock_app: MockVerifyingApp<CodingB, S> =
2820 MockVerifyingApp::new().with_propose_result(block_to_propose);
2821 let cfg = MarshaledConfig {
2822 application: mock_app,
2823 marshal: marshal.clone(),
2824 shards: shards.clone(),
2825 scheme_provider: ConstantProvider::new(schemes[0].clone()),
2826 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
2827 strategy: Sequential,
2828 };
2829 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
2830
2831 let commitment = marshaled
2835 .propose(propose_context)
2836 .await
2837 .await
2838 .expect("propose should produce a commitment");
2839 assert_eq!(commitment, expected_commitment);
2840
2841 assert!(
2844 marshaled
2845 .certify(propose_round, commitment)
2846 .await
2847 .await
2848 .expect("certify result missing"),
2849 "certify must succeed for the leader's own proposal"
2850 );
2851
2852 marshal_actor_handle.abort();
2854 drop(marshaled);
2855 drop(marshal);
2856 drop(shards);
2857
2858 let setup2 = CodingHarness::setup_validator(
2859 context
2860 .child("validator_restart")
2861 .with_attribute("index", 0),
2862 &mut oracle,
2863 me.clone(),
2864 ConstantProvider::new(schemes[0].clone()),
2865 )
2866 .await;
2867 let marshal2 = setup2.mailbox;
2868
2869 let post_restart = marshal2.get_block(&block_digest).await;
2873 assert!(
2874 post_restart.is_some(),
2875 "proposer should recover its own block after restart"
2876 );
2877 });
2878 }
2879
2880 #[test_traced("WARN")]
2885 fn test_marshaled_propose_relay_sends_staged_block() {
2886 let runner = deterministic::Runner::timed(Duration::from_secs(60));
2887 runner.start(|mut context| async move {
2888 let Fixture {
2889 participants,
2890 schemes,
2891 ..
2892 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2893 let mut oracle = setup_network_with_participants(
2894 context.child("network"),
2895 NZUsize!(1),
2896 participants.clone(),
2897 )
2898 .await;
2899
2900 let me = participants[0].clone();
2901 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2902
2903 let setup = CodingHarness::setup_validator(
2904 context.child("validator").with_attribute("index", 0),
2905 &mut oracle,
2906 me.clone(),
2907 ConstantProvider::new(schemes[0].clone()),
2908 )
2909 .await;
2910 let marshal = setup.mailbox;
2911 let shards = setup.extra;
2912
2913 let genesis_ctx = CodingCtx {
2914 round: Round::zero(),
2915 leader: default_leader(),
2916 parent: (View::zero(), genesis_commitment()),
2917 };
2918 let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
2919 let genesis_parent_commitment = genesis_coding_commitment::<Sha256, _>(&genesis);
2920
2921 let propose_round = Round::new(Epoch::zero(), View::new(1));
2922 let propose_context = CodingCtx {
2923 round: propose_round,
2924 leader: me.clone(),
2925 parent: (View::zero(), genesis_parent_commitment),
2926 };
2927 let block_to_propose = make_coding_block(
2928 propose_context.clone(),
2929 genesis.digest(),
2930 Height::new(1),
2931 100,
2932 );
2933 let block_digest = block_to_propose.digest();
2934 let expected_commitment = CodedBlock::<_, ReedSolomon<Sha256>, Sha256>::new(
2935 block_to_propose.clone(),
2936 coding_config,
2937 &Sequential,
2938 )
2939 .commitment();
2940
2941 let mock_app: MockVerifyingApp<CodingB, S> =
2942 MockVerifyingApp::new().with_propose_result(block_to_propose);
2943 let cfg = MarshaledConfig {
2944 application: mock_app,
2945 marshal: marshal.clone(),
2946 shards: shards.clone(),
2947 scheme_provider: ConstantProvider::new(schemes[0].clone()),
2948 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
2949 strategy: Sequential,
2950 };
2951 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
2952
2953 let commitment = marshaled
2954 .propose(propose_context)
2955 .await
2956 .await
2957 .expect("propose should produce a commitment");
2958 assert_eq!(commitment, expected_commitment);
2959
2960 let subscription = shards.subscribe(commitment);
2963 let _ = marshaled.broadcast(
2964 commitment,
2965 Plan::Propose {
2966 round: propose_round,
2967 },
2968 );
2969 let cached = subscription
2970 .await
2971 .expect("shard engine must cache the relayed proposal");
2972 assert_eq!(cached.digest(), block_digest);
2973
2974 assert!(
2977 marshaled
2978 .certify(propose_round, commitment)
2979 .await
2980 .await
2981 .expect("certify result missing"),
2982 "certify must succeed for the relayed proposal"
2983 );
2984 });
2985 }
2986
2987 #[test_traced("WARN")]
2997 fn test_propose_reuses_verified_block_on_restart() {
2998 let runner = deterministic::Runner::timed(Duration::from_secs(60));
2999 runner.start(|mut context| async move {
3000 let Fixture {
3001 participants,
3002 schemes,
3003 ..
3004 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
3005 let mut oracle = setup_network_with_participants(
3006 context.child("network"),
3007 NZUsize!(1),
3008 participants.clone(),
3009 )
3010 .await;
3011
3012 let me = participants[0].clone();
3013 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
3014
3015 let setup = CodingHarness::setup_validator(
3016 context.child("validator").with_attribute("index", 0),
3017 &mut oracle,
3018 me.clone(),
3019 ConstantProvider::new(schemes[0].clone()),
3020 )
3021 .await;
3022 let marshal = setup.mailbox;
3023 let shards = setup.extra;
3024
3025 let genesis_ctx = CodingCtx {
3026 round: Round::zero(),
3027 leader: default_leader(),
3028 parent: (View::zero(), genesis_commitment()),
3029 };
3030 let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
3031 let genesis_parent_commitment = genesis_coding_commitment::<Sha256, _>(&genesis);
3032
3033 let round = Round::new(Epoch::zero(), View::new(1));
3034 let ctx = CodingCtx {
3035 round,
3036 leader: me.clone(),
3037 parent: (View::zero(), genesis_parent_commitment),
3038 };
3039
3040 let block_a = make_coding_block(ctx.clone(), genesis.digest(), Height::new(1), 100);
3042 let coded_a: CodedBlock<_, ReedSolomon<Sha256>, Sha256> =
3043 CodedBlock::new(block_a.clone(), coding_config, &Sequential);
3044 let commitment_a = coded_a.commitment();
3045 assert!(marshal.verified(round, coded_a).await);
3046
3047 let (mock_app, verify_started, _release_verify): (GatedVerifyingApp<CodingB, S>, _, _) =
3053 GatedVerifyingApp::new();
3054 let cfg = MarshaledConfig {
3055 application: mock_app,
3056 marshal: marshal.clone(),
3057 shards: shards.clone(),
3058 scheme_provider: ConstantProvider::new(schemes[0].clone()),
3059 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
3060 strategy: Sequential,
3061 };
3062 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
3063
3064 let commitment = marshaled
3065 .propose(ctx)
3066 .await
3067 .await
3068 .expect("propose must return a commitment");
3069 assert_eq!(
3070 commitment, commitment_a,
3071 "propose must reuse the block marshal already persisted for this round"
3072 );
3073
3074 let _ = marshaled.broadcast(commitment, Plan::Propose { round });
3079 let certify_rx = marshaled.certify(round, commitment).await;
3080 select! {
3081 result = certify_rx => {
3082 assert!(
3083 result.expect("certify result missing"),
3084 "recovered proposal must certify through the relay handshake"
3085 );
3086 },
3087 _ = verify_started => {
3088 panic!("certifying a recovered proposal must not run app verification");
3089 },
3090 }
3091 });
3092 }
3093
3094 #[test_traced("WARN")]
3102 fn test_propose_skips_when_verified_block_context_changed() {
3103 let runner = deterministic::Runner::timed(Duration::from_secs(60));
3104 runner.start(|mut context| async move {
3105 let Fixture {
3106 participants,
3107 schemes,
3108 ..
3109 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
3110 let mut oracle = setup_network_with_participants(
3111 context.child("network"),
3112 NZUsize!(1),
3113 participants.clone(),
3114 )
3115 .await;
3116
3117 let me = participants[0].clone();
3118 let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
3119
3120 let setup = CodingHarness::setup_validator(
3121 context.child("validator").with_attribute("index", 0),
3122 &mut oracle,
3123 me.clone(),
3124 ConstantProvider::new(schemes[0].clone()),
3125 )
3126 .await;
3127 let marshal = setup.mailbox;
3128 let shards = setup.extra;
3129
3130 let genesis_ctx = CodingCtx {
3131 round: Round::zero(),
3132 leader: default_leader(),
3133 parent: (View::zero(), genesis_commitment()),
3134 };
3135 let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
3136 let genesis_parent_commitment = genesis_coding_commitment::<Sha256, _>(&genesis);
3137
3138 let round = Round::new(Epoch::zero(), View::new(2));
3140 let stale_ctx = CodingCtx {
3141 round,
3142 leader: me.clone(),
3143 parent: (View::zero(), genesis_parent_commitment),
3144 };
3145 let stale_block = make_coding_block(stale_ctx, genesis.digest(), Height::new(1), 100);
3146 let stale_coded: CodedBlock<_, ReedSolomon<Sha256>, Sha256> =
3147 CodedBlock::new(stale_block, coding_config, &Sequential);
3148 assert!(marshal.verified(round, stale_coded).await);
3149
3150 let new_parent_commitment = Commitment::from((
3153 Sha256::hash(b"different-parent-block"),
3154 Sha256::hash(b"different-parent-inner"),
3155 Sha256::hash(b"different-parent-ctx"),
3156 coding_config,
3157 ));
3158 let new_ctx = CodingCtx {
3159 round,
3160 leader: me.clone(),
3161 parent: (View::new(1), new_parent_commitment),
3162 };
3163
3164 let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
3165 let cfg = MarshaledConfig {
3166 application: mock_app,
3167 marshal: marshal.clone(),
3168 shards: shards.clone(),
3169 scheme_provider: ConstantProvider::new(schemes[0].clone()),
3170 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
3171 strategy: Sequential,
3172 };
3173 let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
3174
3175 let commitment_rx = marshaled.propose(new_ctx).await;
3176 assert!(
3177 commitment_rx.await.is_err(),
3178 "propose must drop the receiver when the cached block's context no longer matches"
3179 );
3180 });
3181 }
3182}