1use std::{
2 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
3 num::{NonZeroU64, NonZeroUsize},
4 panic::resume_unwind,
5 sync::Arc,
6 time::Duration,
7};
8
9use committable::{Commitment, Committable};
10use hotshot_types::{
11 PeerConfig,
12 consensus::PayloadWithMetadata,
13 data::{EpochNumber, Leaf2, VidCommitment, VidCommitment2, ViewNumber},
14 epoch_membership::EpochMembershipCoordinator,
15 message::UpgradeLock,
16 traits::{
17 BlockPayload, EncodeBytes, ValidatedState as _,
18 block_contents::{BuilderFee, Transaction},
19 metrics::Histogram,
20 node_implementation::NodeType,
21 signature_key::BuilderSignatureKey,
22 },
23 utils::BuilderCommitment,
24};
25use tokio::{
26 task::{AbortHandle, JoinSet, spawn_blocking},
27 time::sleep,
28};
29use tracing::{debug_span, error, warn};
30use versions::{NEW_PROTOCOL_VERSION, NO_BUILDER_COMMITMENT_VERSION, Version};
31
32use crate::{
33 consensus::ConsensusInput,
34 coordinator::metrics::{Measurement, finish_measurement, ignore_measurement},
35 helpers::proposal_commitment,
36 leader_trace::{LeaderEvent, LeaderTracerHandle},
37 message::{DedupManifest, Proposal, TransactionMessage},
38 network::message_limit,
39 state::HeaderRequest,
40 trace_leader_event,
41 vid::{BuiltDispersal, VidDisperseError},
42};
43
44#[derive(Debug, thiserror::Error)]
45pub enum BlockError {
46 #[error("payload construction failed: {0}")]
47 PayloadConstruction(String),
48
49 #[error("stake table unavailable")]
50 StakeTableUnavailable,
51
52 #[error("builder signature failed")]
53 BuilderSignature,
54
55 #[error("vid dispersal failed: {0}")]
56 Vid(#[from] VidDisperseError),
57
58 #[error("version {0} predates the new protocol")]
59 UnsupportedVersion(Version),
60
61 #[error("block builder task cancelled")]
62 Cancelled,
63}
64
65#[derive(Debug, thiserror::Error)]
67pub enum SubmitError {
68 #[error("transaction of {size} bytes exceeds the {limit} byte limit")]
70 TooLarge { size: u64, limit: u64 },
71 #[error("transaction retry buffer is full")]
73 RetryBufferFull,
74}
75
76#[derive(Clone, Eq, PartialEq, Debug)]
77pub struct BlockAndHeaderRequest<T: NodeType> {
78 pub view: ViewNumber,
79 pub epoch: EpochNumber,
80 pub parent_proposal: Proposal<T>,
81}
82
83pub struct BlockBuilderOutput<T: NodeType> {
84 pub view: ViewNumber,
85 pub epoch: EpochNumber,
86 pub payload: PayloadWithMetadata<T>,
87 pub parent_proposal: Proposal<T>,
88 pub builder_commitment: BuilderCommitment,
89 pub builder_fee: BuilderFee<T>,
90 pub payload_commitment: VidCommitment,
91 pub manifest: DedupManifest<T>,
92 pub dispersal: Option<BuiltDispersal<T>>,
95}
96
97pub struct BlockCommitments<T: NodeType> {
99 pub block_size: u64,
100 pub payload_commitment: VidCommitment,
101 pub builder_commitment: BuilderCommitment,
102 pub hashes: Vec<Commitment<T::Transaction>>,
103 pub dispersal: BuiltDispersal<T>,
104}
105
106pub fn block_commitments<T: NodeType>(
115 payload: &PayloadWithMetadata<T>,
116 pooled_hashes: &[Commitment<T::Transaction>],
117 stake_table: &[PeerConfig<T>],
118 version: Version,
119) -> Result<BlockCommitments<T>, BlockError> {
120 if version < NEW_PROTOCOL_VERSION {
121 return Err(BlockError::UnsupportedVersion(version));
122 }
123 let _span = debug_span!("block_commitments").entered();
124 let (payload_bytes, metadata_bytes) =
125 debug_span!("encode").in_scope(|| (payload.payload.encode(), payload.metadata.encode()));
126 let block_size = payload_bytes.len() as u64;
127 let (hashes, (dispersal, builder_commitment)) = rayon::join(
130 || transaction_hashes(payload, pooled_hashes),
131 || {
132 rayon::join(
133 || {
134 debug_span!("vid_encode").in_scope(|| {
135 BuiltDispersal::new(stake_table, payload_bytes, &metadata_bytes)
136 })
137 },
138 || {
141 if version >= NO_BUILDER_COMMITMENT_VERSION {
142 BuilderCommitment::empty()
143 } else {
144 debug_span!("builder_commitment")
145 .in_scope(|| payload.payload.builder_commitment(&payload.metadata))
146 }
147 },
148 )
149 },
150 );
151 let dispersal = dispersal?;
152 Ok(BlockCommitments {
153 block_size,
154 payload_commitment: VidCommitment::V2(dispersal.payload_commitment()?),
155 builder_commitment,
156 hashes,
157 dispersal,
158 })
159}
160
161pub(crate) fn transaction_hashes<T: NodeType>(
167 payload: &PayloadWithMetadata<T>,
168 pooled_hashes: &[Commitment<T::Transaction>],
169) -> Vec<Commitment<T::Transaction>> {
170 if payload.payload.num_transactions(&payload.metadata) == pooled_hashes.len() {
171 debug_assert_eq!(
172 pooled_hashes.iter().copied().collect::<HashSet<_>>(),
173 payload
174 .payload
175 .transaction_commitments(&payload.metadata)
176 .into_iter()
177 .collect::<HashSet<_>>(),
178 "a payload that kept every pooled transaction holds exactly those"
179 );
180 pooled_hashes.to_vec()
181 } else {
182 debug_span!("transaction_hashes")
183 .in_scope(|| payload.payload.transaction_commitments(&payload.metadata))
184 }
185}
186
187const FORWARD_ENVELOPE_BYTES: u64 = 4096;
189
190const SEND_LEAD: u64 = 2;
194
195pub struct BlockBuilderConfig {
196 pub max_retry_bytes: u64,
197 pub block_sizes: BTreeMap<Version, u64>,
199 pub ttl: u64,
200 pub dedup_window_size: u64,
203 pub empty_block_delay: Duration,
204 pub fanout: NonZeroU64,
207}
208
209impl Default for BlockBuilderConfig {
210 fn default() -> Self {
211 let ttl = 10;
212 Self {
213 max_retry_bytes: 100 * 1024 * 1024,
214 block_sizes: BTreeMap::from([(versions::version(0, 0), 2 * 1024 * 1024)]),
215 ttl,
216 dedup_window_size: ttl,
217 empty_block_delay: Duration::from_millis(500),
218 fanout: NonZeroU64::new(2).expect("2 is non-zero"),
219 }
220 }
221}
222
223pub fn forward_budget(max_message_size: NonZeroUsize) -> u64 {
225 (max_message_size.get() as u64).saturating_sub(FORWARD_ENVELOPE_BYTES)
226}
227
228struct RetryEntry<T: NodeType> {
229 tx: T::Transaction,
230 valid_until: ViewNumber,
231 size: u64,
232 encoded_size: u64,
234 sent_until: ViewNumber,
236}
237
238struct PoolEntry<T: NodeType> {
239 tx: T::Transaction,
240 view: ViewNumber,
242}
243
244const fn retry_entry_overhead<T: NodeType>() -> u64 {
247 let map = size_of::<(Commitment<T::Transaction>, RetryEntry<T>)>();
248 let order = size_of::<(ViewNumber, Commitment<T::Transaction>)>();
249 2 * (map + order) as u64
250}
251
252#[derive(Clone)]
254struct PooledBlock<T: NodeType> {
255 txs: Arc<Vec<T::Transaction>>,
256 hashes: Arc<Vec<Commitment<T::Transaction>>>,
257}
258
259pub struct BlockBuilder<T: NodeType> {
260 instance: Arc<T::InstanceState>,
261 membership: EpochMembershipCoordinator<T>,
262 retry_pending: HashMap<Commitment<T::Transaction>, RetryEntry<T>>,
263 retry_order: BTreeSet<(ViewNumber, Commitment<T::Transaction>)>,
264 retry_total_bytes: u64,
265 leader_buffer: HashMap<Commitment<T::Transaction>, PoolEntry<T>>,
266 leader_order: BTreeSet<(ViewNumber, Commitment<T::Transaction>)>,
267 leader_total_bytes: u64,
268 dedups: BTreeMap<ViewNumber, HashSet<Commitment<T::Transaction>>>,
269 undecided: BTreeMap<(ViewNumber, VidCommitment2), Vec<Commitment<T::Transaction>>>,
272 decided: BTreeSet<(ViewNumber, VidCommitment2)>,
275 config: BlockBuilderConfig,
276 upgrade_lock: UpgradeLock<T>,
277 current_view: ViewNumber,
278 calculations: BTreeMap<(ViewNumber, Commitment<Leaf2<T>>), AbortHandle>,
283 view_transactions: BTreeMap<ViewNumber, PooledBlock<T>>,
291 tasks: JoinSet<Result<BlockBuilderOutput<T>, BlockError>>,
292 commitments_duration_metric: Option<Arc<dyn Histogram>>,
293 tracer: Option<LeaderTracerHandle>,
296}
297
298impl<T: NodeType> BlockBuilder<T> {
299 pub fn new(
300 instance: Arc<T::InstanceState>,
301 membership: EpochMembershipCoordinator<T>,
302 config: BlockBuilderConfig,
303 upgrade_lock: UpgradeLock<T>,
304 ) -> Self {
305 Self {
306 instance,
307 membership,
308 config,
309 upgrade_lock,
310 retry_pending: HashMap::new(),
311 retry_order: BTreeSet::new(),
312 retry_total_bytes: 0,
313 leader_buffer: HashMap::new(),
314 leader_order: BTreeSet::new(),
315 leader_total_bytes: 0,
316 dedups: BTreeMap::new(),
317 undecided: BTreeMap::new(),
318 decided: BTreeSet::new(),
319 current_view: ViewNumber::genesis(),
320 calculations: BTreeMap::new(),
321 view_transactions: BTreeMap::new(),
322 tasks: JoinSet::new(),
323 commitments_duration_metric: None,
324 tracer: None,
325 }
326 }
327
328 pub fn with_tracer(mut self, tracer: Option<LeaderTracerHandle>) -> Self {
330 self.tracer = tracer;
331 self
332 }
333
334 pub fn with_metrics(mut self, commitments_duration: Option<Arc<dyn Histogram>>) -> Self {
335 self.commitments_duration_metric = commitments_duration;
336 self
337 }
338
339 pub fn request_block(&mut self, request: BlockAndHeaderRequest<T>) {
340 let view = request.view;
341 let parent_commitment = proposal_commitment(&request.parent_proposal);
342 if self.calculations.contains_key(&(view, parent_commitment)) {
343 return;
344 }
345 let Ok(version) = self.upgrade_lock.version(view) else {
346 warn!(%view, "unsupported version");
347 return;
348 };
349 let epoch = request.epoch;
350 let PooledBlock {
351 txs,
352 hashes: pooled_hashes,
353 } = self.transactions_for(view);
354 let instance = self.instance.clone();
355 let membership = self.membership.clone();
356
357 let empty_block_delay = self.config.empty_block_delay;
358 let duration_metric = self.commitments_duration_metric.clone();
359 let tracer = self.tracer.clone();
360
361 trace_leader_event!(tracer, view, LeaderEvent::BlockRequestStarted);
362 let handle = self.tasks.spawn(async move {
363 if txs.is_empty() {
366 sleep(empty_block_delay).await;
367 }
368 trace_leader_event!(tracer, view, LeaderEvent::BlockPayloadStart);
369
370 let validated_state =
371 T::ValidatedState::from_header(&request.parent_proposal.block_header);
372 let (payload, metadata) =
373 T::BlockPayload::from_transactions(&txs, &validated_state, &instance)
374 .await
375 .map_err(|e| BlockError::PayloadConstruction(e.to_string()))?;
376 let payload: PayloadWithMetadata<T> = PayloadWithMetadata { payload, metadata };
377
378 let stake_table: Vec<_> = membership
379 .stake_table_for_epoch(Some(epoch))
380 .map_err(|_| BlockError::StakeTableUnavailable)?
381 .stake_table()
382 .cloned()
383 .collect();
384 let commitments = spawn_blocking(move || {
385 let measurement = duration_metric.map(Measurement::start);
386 trace_leader_event!(tracer, view, LeaderEvent::BlockCommitmentsStart);
387 let commitments =
388 block_commitments(&payload, &pooled_hashes, &stake_table, version);
389 trace_leader_event!(tracer, view, LeaderEvent::BlockCommitmentsEnd);
390 match &commitments {
391 Ok(_) => finish_measurement(measurement),
392 Err(_) => ignore_measurement(measurement),
393 }
394 (payload, commitments)
395 });
396 let (payload, commitments) = match commitments.await {
397 Ok(out) => out,
398 Err(e) if e.is_panic() => resume_unwind(e.into_panic()),
399 Err(_) => return Err(BlockError::Cancelled),
400 };
401 let BlockCommitments {
402 block_size,
403 payload_commitment,
404 builder_commitment,
405 hashes,
406 dispersal,
407 } = commitments?;
408 let manifest = DedupManifest {
409 view,
410 epoch,
411 hashes,
412 };
413 let (builder_key, builder_private_key) =
414 T::BuilderSignatureKey::generated_from_seed_indexed([0u8; 32], 0);
415 let offered_fee = block_size;
416 let builder_fee = BuilderFee {
417 fee_amount: offered_fee,
418 fee_account: builder_key,
419 fee_signature: T::BuilderSignatureKey::sign_fee(
420 &builder_private_key,
421 offered_fee,
422 &payload.metadata,
423 )
424 .map_err(|_| BlockError::BuilderSignature)?,
425 };
426 Ok(BlockBuilderOutput {
427 view,
428 epoch,
429 payload,
430 parent_proposal: request.parent_proposal,
431 builder_commitment,
432 builder_fee,
433 payload_commitment,
434 manifest,
435 dispersal: Some(dispersal),
436 })
437 });
438 self.calculations.insert((view, parent_commitment), handle);
439 }
440
441 fn transactions_for(&mut self, view: ViewNumber) -> PooledBlock<T> {
442 if let Some(pooled) = self.view_transactions.get(&view) {
443 return pooled.clone();
444 }
445 let (txs, hashes) = self.take_block(view);
446 let pooled = PooledBlock {
447 txs: Arc::new(txs),
448 hashes: Arc::new(hashes),
449 };
450 self.view_transactions.insert(view, pooled.clone());
451 pooled
452 }
453
454 fn take_block(
457 &mut self,
458 view: ViewNumber,
459 ) -> (Vec<T::Transaction>, Vec<Commitment<T::Transaction>>) {
460 let max_bytes = self.block_size(view);
461 let mut bytes = 0;
462 let mut taken = Vec::new();
463 for (_, hash) in &self.leader_order {
464 let size = self.leader_buffer[hash].tx.minimum_block_size();
465 if bytes + size > max_bytes {
466 continue;
467 }
468 bytes += size;
469 taken.push(*hash);
470 }
471 let txs = taken
472 .iter()
473 .map(|hash| {
474 self.remove_pooled(hash)
475 .expect("hashes come from the pool's own order")
476 })
477 .collect();
478 (txs, taken)
479 }
480
481 fn remove_pooled(&mut self, hash: &Commitment<T::Transaction>) -> Option<T::Transaction> {
482 let entry = self.leader_buffer.remove(hash)?;
483 self.leader_order.remove(&(entry.view, *hash));
484 self.leader_total_bytes -= entry.tx.minimum_block_size();
485 Some(entry.tx)
486 }
487
488 pub async fn next(&mut self) -> Option<Result<BlockBuilderOutput<T>, BlockError>> {
489 loop {
490 match self.tasks.join_next().await {
491 Some(Ok(result)) => return Some(result),
492 Some(Err(err)) => {
493 if err.is_panic() {
494 error!(%err, "block builder task panicked");
495 }
496 },
497 None => return None,
498 }
499 }
500 }
501
502 pub fn gc(&mut self, view_number: ViewNumber) {
503 self.calculations.retain(|(view, _), handle| {
504 if *view < view_number {
505 handle.abort();
506 false
507 } else {
508 true
509 }
510 });
511 self.view_transactions = self.view_transactions.split_off(&view_number);
512 }
513
514 pub fn fanout(&self) -> NonZeroU64 {
515 self.config.fanout
516 }
517
518 pub fn start_at(&mut self, view: ViewNumber) {
522 self.current_view = view;
523 }
524
525 pub fn outstanding_transactions(&self) -> (usize, usize) {
526 (self.retry_pending.len(), self.retry_total_bytes as usize)
527 }
528
529 pub fn on_submit_transaction(
533 &mut self,
534 tx: T::Transaction,
535 ) -> Result<Option<TransactionMessage<T>>, SubmitError> {
536 let hash = tx.commit();
537
538 if self.retry_pending.contains_key(&hash) {
539 return Ok(None);
540 }
541
542 let size = tx.minimum_block_size();
543 let encoded_size = bincode::serialized_size(&tx).expect("transactions serialize");
544 let max_bytes = self
546 .block_size(self.current_view)
547 .max(self.block_size(self.current_view + SEND_LEAD));
548 let budget = forward_budget(message_limit(max_bytes));
549 if size > max_bytes {
550 return Err(SubmitError::TooLarge {
551 size,
552 limit: max_bytes,
553 });
554 }
555 if encoded_size > budget {
556 return Err(SubmitError::TooLarge {
557 size: encoded_size,
558 limit: budget,
559 });
560 }
561 let charge = size + retry_entry_overhead::<T>();
562 if self.retry_total_bytes + charge > self.config.max_retry_bytes {
563 warn!("retry buffer full, rejecting {hash}");
564 return Err(SubmitError::RetryBufferFull);
565 }
566
567 let valid_until = self.current_view + self.config.ttl;
568 let first_target = self.current_view + SEND_LEAD;
569 let sent_until = first_target + (self.config.fanout.get() - 1);
570 let message = TransactionMessage {
571 view: first_target,
572 transactions: Vec::from([tx.clone()]),
573 };
574
575 self.retry_total_bytes += charge;
576 self.retry_order.insert((valid_until, hash));
577 self.retry_pending.insert(
578 hash,
579 RetryEntry {
580 tx,
581 valid_until,
582 size,
583 encoded_size,
584 sent_until,
585 },
586 );
587 Ok(Some(message))
588 }
589
590 pub fn on_transactions(&mut self, msg: TransactionMessage<T>) {
591 let view = msg.view.max(self.current_view);
594 let block_size = self.block_size(view);
595 let max_bytes = block_size.saturating_mul(self.config.fanout.get() + 1);
596 for tx in msg.transactions {
597 let hash = tx.commit();
598
599 if self.dedups.values().any(|hs| hs.contains(&hash)) {
600 continue;
601 }
602
603 if self.leader_buffer.contains_key(&hash) {
604 continue;
605 }
606
607 let size = tx.minimum_block_size();
608 if size > block_size {
610 continue;
611 }
612 if self.leader_total_bytes + size > max_bytes {
613 continue;
614 }
615
616 self.leader_total_bytes += size;
617 self.leader_order.insert((view, hash));
618 self.leader_buffer.insert(hash, PoolEntry { tx, view });
619 }
620 }
621
622 pub fn on_dedup_manifest(&mut self, manifest: DedupManifest<T>) {
623 let DedupManifest { view, hashes, .. } = manifest;
624 self.mark_included(view, hashes);
625 }
626
627 pub fn on_view_changed(&mut self, view: ViewNumber) -> Option<TransactionMessage<T>> {
631 self.current_view = view;
632 while let Some(&(valid_until, hash)) = self.retry_order.first() {
633 if valid_until >= view {
634 break;
635 }
636 self.remove_pending(&hash);
637 }
638 self.expire_pooled(view);
639 self.expire_block_records(view);
640
641 let batch = self.resend_batch(view);
642 if batch.is_empty() {
643 return None;
644 }
645 Some(TransactionMessage {
646 view: view + SEND_LEAD,
647 transactions: batch,
648 })
649 }
650
651 fn resend_batch(&mut self, view: ViewNumber) -> Vec<T::Transaction> {
652 let first_target = view + SEND_LEAD;
653 let max_bytes = self.block_size(first_target);
654 let max_encoded = forward_budget(message_limit(max_bytes));
655 let mut batch = Vec::new();
656 let mut unfit = Vec::new();
657 let (mut bytes, mut encoded) = (0u64, 0u64);
658 for (_, hash) in &self.retry_order {
659 let entry = &self.retry_pending[hash];
660 if entry.size > max_bytes || entry.encoded_size > max_encoded {
661 unfit.push(*hash);
662 continue;
663 }
664 if entry.sent_until + 1 >= view {
668 continue;
669 }
670 if bytes + entry.size > max_bytes || encoded + entry.encoded_size > max_encoded {
671 continue;
672 }
673 bytes += entry.size;
674 encoded += entry.encoded_size;
675 batch.push(*hash);
676 }
677 for hash in &unfit {
678 warn!(%hash, "pending transaction no longer fits a block, dropping");
679 self.remove_pending(hash);
680 }
681 let sent_until = first_target + (self.config.fanout.get() - 1);
682 batch
683 .into_iter()
684 .map(|hash| {
685 let entry = self
686 .retry_pending
687 .get_mut(&hash)
688 .expect("batched hashes come from the retry buffer");
689 entry.sent_until = sent_until;
690 entry.tx.clone()
691 })
692 .collect()
693 }
694
695 fn expire_pooled(&mut self, view: ViewNumber) {
698 while let Some(&(sent_for, hash)) = self.leader_order.first() {
699 if sent_for + self.config.ttl >= view {
700 break;
701 }
702 self.remove_pooled(&hash);
703 }
704 }
705
706 fn expire_block_records(&mut self, view: ViewNumber) {
710 let ttl = self.config.ttl;
711 self.undecided
712 .retain(|(block_view, _), _| *block_view + ttl >= view);
713 self.decided
714 .retain(|(block_view, _)| *block_view + ttl >= view);
715 }
716
717 fn remove_pending(&mut self, hash: &Commitment<T::Transaction>) {
718 if let Some(entry) = self.retry_pending.remove(hash) {
719 self.retry_order.remove(&(entry.valid_until, *hash));
720 self.retry_total_bytes -= entry.size + retry_entry_overhead::<T>();
721 }
722 }
723
724 fn block_size(&self, view: ViewNumber) -> u64 {
726 let version = self.upgrade_lock.version_infallible(view);
727 *self
728 .config
729 .block_sizes
730 .range(..=version)
731 .next_back()
732 .expect("block sizes start at or below the running version")
733 .1
734 }
735
736 pub fn on_block_reconstructed(
741 &mut self,
742 view: ViewNumber,
743 payload_commitment: VidCommitment2,
744 tx_commitments: Vec<Commitment<T::Transaction>>,
745 ) {
746 if self.decided.contains(&(view, payload_commitment)) {
747 for hash in &tx_commitments {
749 self.remove_pending(hash);
750 }
751 } else {
752 self.undecided
753 .insert((view, payload_commitment), tx_commitments.clone());
754 }
755 self.mark_included(view, tx_commitments);
756 }
757
758 pub fn on_blocks_decided(
762 &mut self,
763 blocks: impl IntoIterator<Item = (ViewNumber, VidCommitment2)>,
764 ) {
765 for key in blocks {
766 for hash in self.undecided.remove(&key).unwrap_or_default() {
767 self.remove_pending(&hash);
768 }
769 self.decided.insert(key);
770 }
771 }
772
773 fn mark_included(&mut self, view: ViewNumber, hashes: Vec<Commitment<T::Transaction>>) {
774 for hash in &hashes {
775 self.remove_pooled(hash);
776 }
777
778 let lower_bound: ViewNumber = self
779 .current_view
780 .saturating_sub(self.config.dedup_window_size)
781 .into();
782
783 if view >= lower_bound {
784 self.dedups.entry(view).or_default().extend(hashes);
785 }
786
787 self.dedups = self.dedups.split_off(&lower_bound);
788 }
789
790 #[cfg(test)]
791 pub(crate) fn drain(&mut self, view: ViewNumber) -> Vec<T::Transaction> {
792 self.take_block(view).0
793 }
794}
795
796impl<T: NodeType> From<&BlockBuilderOutput<T>> for HeaderRequest<T> {
797 fn from(output: &BlockBuilderOutput<T>) -> Self {
798 HeaderRequest {
799 view: output.view,
800 epoch: output.epoch,
801 parent_proposal: output.parent_proposal.clone(),
802 payload_commitment: output.payload_commitment,
803 builder_commitment: output.builder_commitment.clone(),
804 metadata: output.payload.metadata.clone(),
805 builder_fee: output.builder_fee.clone(),
806 }
807 }
808}
809
810impl<T: NodeType> From<BlockBuilderOutput<T>> for ConsensusInput<T> {
811 fn from(output: BlockBuilderOutput<T>) -> Self {
812 ConsensusInput::BlockBuilt {
813 view: output.view,
814 epoch: output.epoch,
815 payload: output.payload.payload,
816 metadata: output.payload.metadata,
817 payload_commitment: output.payload_commitment,
818 }
819 }
820}