Skip to main content

hotshot_new_protocol/
state.rs

1use std::{
2    collections::{BTreeMap, HashMap},
3    sync::Arc,
4};
5
6use committable::{Commitment, Committable};
7use hotshot_types::{
8    data::{BlockNumber, EpochNumber, Leaf2, VidCommitment, ViewNumber},
9    message::UpgradeLock,
10    traits::{
11        BlockPayload, ValidatedState,
12        block_contents::{BlockHeader, BuilderFee},
13        metrics::Histogram,
14        node_implementation::NodeType,
15    },
16    utils::BuilderCommitment,
17    vote::HasViewNumber,
18};
19use tokio::task::{AbortHandle, JoinSet};
20use tracing::{debug, error, warn};
21
22use crate::{
23    coordinator::metrics::{Measurement, finish_measurement, ignore_measurement},
24    helpers::proposal_commitment,
25    message::Proposal,
26};
27
28/// Validated states stay this many views below the decided view so peers can serve catchup.
29const STATE_GC_MARGIN: u64 = 8;
30
31pub struct UpdateLeaf<T: NodeType> {
32    pub view: ViewNumber,
33    pub leaf: Leaf2<T>,
34    pub state: Arc<T::ValidatedState>,
35    pub delta: Option<Delta<T>>,
36}
37
38#[derive(Clone, Debug, Eq, PartialEq)]
39pub struct StateRequest<T: NodeType> {
40    pub view: ViewNumber,
41    pub parent_view: ViewNumber,
42    pub epoch: EpochNumber,
43    pub block: BlockNumber,
44    pub proposal: Proposal<T>,
45    pub parent_commitment: Commitment<Leaf2<T>>,
46    pub payload_size: Option<u32>,
47}
48
49#[derive(Clone, Debug, Eq, PartialEq)]
50pub struct HeaderRequest<T: NodeType> {
51    pub view: ViewNumber,
52    pub epoch: EpochNumber,
53    pub parent_proposal: Proposal<T>,
54    pub payload_commitment: VidCommitment,
55    pub builder_commitment: BuilderCommitment,
56    pub metadata: <T::BlockPayload as BlockPayload<T>>::Metadata,
57    pub builder_fee: BuilderFee<T>,
58}
59
60#[derive(Clone, Debug, Eq, PartialEq)]
61pub struct StateResponse<T: NodeType> {
62    pub view: ViewNumber,
63    pub commitment: Commitment<Leaf2<T>>,
64    pub state: Arc<T::ValidatedState>,
65    pub delta: Option<Delta<T>>,
66}
67
68#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct HeaderResponse<T: NodeType> {
70    pub view: ViewNumber,
71    pub epoch: EpochNumber,
72    pub parent_proposal: Proposal<T>,
73}
74
75#[derive(Clone, Debug, Eq, PartialEq)]
76#[allow(clippy::large_enum_variant)]
77pub enum StateManagerOutput<T: NodeType> {
78    State {
79        response: StateResponse<T>,
80        validated: bool,
81    },
82    Header {
83        response: HeaderResponse<T>,
84        header: Option<T::BlockHeader>,
85    },
86}
87
88type Delta<T> = Arc<<<T as NodeType>::ValidatedState as ValidatedState<T>>::Delta>;
89
90#[derive(Clone)]
91pub struct StateEntry<T: NodeType> {
92    pub state: Arc<T::ValidatedState>,
93    pub delta: Option<Delta<T>>,
94    pub leaf: Leaf2<T>,
95}
96
97pub struct StateManager<T: NodeType> {
98    instance: Arc<T::InstanceState>,
99    validated_states: BTreeMap<Commitment<Leaf2<T>>, StateEntry<T>>,
100    state_requests: HashMap<Commitment<Leaf2<T>>, InFlight<T>>,
101    header_requests: HashMap<(ViewNumber, Commitment<Leaf2<T>>), AbortHandle>,
102    pending_requests: HashMap<Commitment<Leaf2<T>>, Vec<Pending<T>>>,
103    /// Newest decided leaf whose in-flight validation made `seed_decided` skip it.
104    /// Cleared by gc below the gc view.
105    decided_in_flight: Option<Leaf2<T>>,
106    upgrade_lock: UpgradeLock<T>,
107    tasks: JoinSet<Completed<T>>,
108    validate_duration_metric: Option<Arc<dyn Histogram>>,
109    update_leaf_duration_metric: Option<Arc<dyn Histogram>>,
110}
111
112/// The proposal lets `gc` seed a stub for a validation it aborts.
113struct InFlight<T: NodeType> {
114    handle: AbortHandle,
115    view: ViewNumber,
116    proposal: Proposal<T>,
117}
118
119enum Pending<T: NodeType> {
120    State(StateRequest<T>),
121    Header(HeaderRequest<T>),
122}
123
124impl<T: NodeType> Pending<T> {
125    fn view(&self) -> ViewNumber {
126        match self {
127            Pending::State(r) => r.view,
128            Pending::Header(r) => r.view,
129        }
130    }
131}
132
133enum Completed<T: NodeType> {
134    State {
135        response: StateResponse<T>,
136        leaf: Option<Leaf2<T>>,
137    },
138    Header {
139        response: HeaderResponse<T>,
140        header: Option<T::BlockHeader>,
141    },
142}
143
144impl<T: NodeType> StateManager<T> {
145    pub fn new(instance: Arc<T::InstanceState>, upgrade_lock: UpgradeLock<T>) -> Self {
146        Self {
147            instance,
148            validated_states: BTreeMap::new(),
149            state_requests: HashMap::new(),
150            header_requests: HashMap::new(),
151            pending_requests: HashMap::new(),
152            decided_in_flight: None,
153            upgrade_lock,
154            tasks: JoinSet::new(),
155            validate_duration_metric: None,
156            update_leaf_duration_metric: None,
157        }
158    }
159
160    pub fn with_metrics(
161        mut self,
162        validate: Option<Arc<dyn Histogram>>,
163        update_leaf: Option<Arc<dyn Histogram>>,
164    ) -> Self {
165        self.validate_duration_metric = validate;
166        self.update_leaf_duration_metric = update_leaf;
167        self
168    }
169
170    /// Get the validated state for a given view.
171    ///
172    /// Lookup is by view, so below the decided view this may return a state from an undecided fork.
173    pub fn get_state(&self, view: ViewNumber) -> Option<&StateEntry<T>> {
174        self.validated_states
175            .iter()
176            .find(|(_, entry)| entry.leaf.view_number() == view)
177            .map(|(_, entry)| entry)
178    }
179
180    /// Get the leaf for a given view
181    pub fn get_leaf(&self, view: ViewNumber) -> Option<Leaf2<T>> {
182        self.validated_states
183            .iter()
184            .find(|(_, entry)| entry.leaf.view_number() == view)
185            .map(|(_, entry)| entry.leaf.clone())
186    }
187
188    pub fn seed_state(&mut self, view: ViewNumber, state: Arc<T::ValidatedState>, leaf: Leaf2<T>) {
189        self.insert_state(view, state, None, leaf);
190    }
191
192    /// Seed a commitment-only (`from_header`) state so a child proposal can be
193    /// validated against this leaf via catchup instead of being dropped.
194    ///
195    /// A real (validated) state for the leaf is never displaced, and any
196    /// header/state requests queued on this leaf are restarted, or re-queued while
197    /// its validation is in flight.
198    pub(crate) fn seed_from_header(&mut self, proposal: Proposal<T>) {
199        let commitment = proposal_commitment(&proposal);
200        if !self.validated_states.contains_key(&commitment) {
201            self.insert_empty_state(proposal);
202        }
203        self.start_pending(commitment);
204    }
205
206    /// Seed a stub for a decided leaf so work queued on it runs via catchup.
207    /// No-op when the leaf is validated. When it is being validated, the stub is
208    /// seeded only if that validation fails. An older leaf never replaces a newer one.
209    pub(crate) fn seed_decided(&mut self, leaf: Leaf2<T>) {
210        let commitment = leaf.commit();
211        if self.validated_states.contains_key(&commitment) {
212            return;
213        }
214        if self.state_requests.contains_key(&commitment) {
215            let newest = self
216                .decided_in_flight
217                .as_ref()
218                .is_none_or(|held| held.view_number() <= leaf.view_number());
219            if newest {
220                self.decided_in_flight = Some(leaf);
221            }
222            return;
223        }
224        let state = T::ValidatedState::from_header(leaf.block_header());
225        self.insert_state(leaf.view_number(), Arc::new(state), None, leaf);
226        self.start_pending(commitment);
227    }
228
229    pub fn request_state(&mut self, request: StateRequest<T>) {
230        let commitment = proposal_commitment(&request.proposal);
231        if self.state_requests.contains_key(&commitment) {
232            return;
233        }
234
235        if self.state_requests.contains_key(&request.parent_commitment) {
236            self.pending_requests
237                .entry(request.parent_commitment)
238                .or_default()
239                .push(Pending::State(request));
240            return;
241        }
242
243        let Some(parent_entry) = self
244            .validated_states
245            .get(&request.parent_commitment)
246            .cloned()
247        else {
248            warn!(
249                view = %request.view,
250                parent_view = %request.parent_view,
251                epoch = %request.epoch,
252                block = %request.block,
253                parent_commitment = %request.parent_commitment,
254                "parent state unavailable; queued on parent for retry. \
255                 If this persists, the parent state never arrived and the node cannot vote."
256            );
257            let queued = self
258                .pending_requests
259                .entry(request.parent_commitment)
260                .or_default();
261            if !queued
262                .iter()
263                .any(|p| matches!(p, Pending::State(r) if r.view == request.view))
264            {
265                queued.push(Pending::State(request));
266            }
267            return;
268        };
269
270        let instance = self.instance.clone();
271        let header = request.proposal.block_header.clone();
272        let proposal = request.proposal.clone();
273        let view = request.view;
274        let payload_size = request.payload_size;
275
276        let Ok(upgrade_lock) = self.upgrade_lock.version(view) else {
277            error!(%view, "unsupported version");
278            return;
279        };
280
281        let duration_metric = self.validate_duration_metric.clone();
282        let handle = self.tasks.spawn(async move {
283            let measurement = duration_metric.map(Measurement::start);
284            let result = parent_entry
285                .state
286                .validate_and_apply_header(
287                    &instance,
288                    &parent_entry.leaf,
289                    &header,
290                    payload_size,
291                    upgrade_lock,
292                    *view,
293                )
294                .await;
295            match result {
296                Ok((state, delta)) => {
297                    finish_measurement(measurement);
298                    Completed::State {
299                        response: StateResponse {
300                            view,
301                            commitment,
302                            state: Arc::new(state),
303                            delta: Some(Arc::new(delta)),
304                        },
305                        leaf: Some(request.proposal.into()),
306                    }
307                },
308                Err(err) => {
309                    ignore_measurement(measurement);
310                    warn!(%err, "state validation failed");
311                    Completed::State {
312                        response: StateResponse {
313                            view,
314                            commitment,
315                            state: Arc::new(T::ValidatedState::from_header(&header)),
316                            delta: None,
317                        },
318                        leaf: None,
319                    }
320                },
321            }
322        });
323
324        self.state_requests.insert(
325            commitment,
326            InFlight {
327                handle,
328                view,
329                proposal,
330            },
331        );
332    }
333
334    pub fn request_header(&mut self, request: HeaderRequest<T>) {
335        let parent_commitment = proposal_commitment(&request.parent_proposal);
336        if self
337            .header_requests
338            .contains_key(&(request.view, parent_commitment))
339        {
340            return;
341        }
342
343        if self.state_requests.contains_key(&parent_commitment) {
344            self.pending_requests
345                .entry(parent_commitment)
346                .or_default()
347                .push(Pending::Header(request));
348            return;
349        }
350
351        let Some(parent_entry) = self.validated_states.get(&parent_commitment).cloned() else {
352            // Parent state not available yet (e.g. its proposal is still
353            // being validated).  Queue the request so it is retried once
354            // the state for the parent view is inserted.
355            self.pending_requests
356                .entry(parent_commitment)
357                .or_default()
358                .push(Pending::Header(request));
359            return;
360        };
361
362        let instance = self.instance.clone();
363        let view = request.view;
364        let epoch = request.epoch;
365        let parent_proposal = request.parent_proposal;
366
367        let Ok(version) = self.upgrade_lock.version(view) else {
368            error!(%view, "unsupported version");
369            return;
370        };
371
372        let handle = self.tasks.spawn(async move {
373            let result = T::BlockHeader::new(
374                &parent_entry.state,
375                &instance,
376                &parent_entry.leaf,
377                request.payload_commitment,
378                request.builder_commitment,
379                request.metadata,
380                request.builder_fee,
381                version,
382                *view,
383            )
384            .await;
385            match result {
386                Ok(header) => Completed::Header {
387                    response: HeaderResponse {
388                        view,
389                        epoch,
390                        parent_proposal,
391                    },
392                    header: Some(header),
393                },
394                Err(err) => {
395                    warn!(%err, "header creation failed");
396                    Completed::Header {
397                        response: HeaderResponse {
398                            view,
399                            epoch,
400                            parent_proposal,
401                        },
402                        header: None,
403                    }
404                },
405            }
406        });
407
408        self.header_requests
409            .insert((view, parent_commitment), handle);
410    }
411
412    /// Provide an externally-obtained validated state.
413    ///
414    /// An update without a delta only enriches an existing entry and is
415    /// otherwise dropped.
416    pub fn update_state(&mut self, update: UpdateLeaf<T>) {
417        let UpdateLeaf {
418            view,
419            leaf,
420            state,
421            delta,
422        } = update;
423        let commitment = leaf.commit();
424        if delta.is_none() {
425            if self.validated_states.contains_key(&commitment) {
426                self.insert_state(view, state, delta, leaf);
427            } else {
428                debug!(%view, %commitment, "dropping delta-less update: no entry for leaf");
429            }
430            return;
431        }
432        self.insert_state(view, state, delta, leaf);
433        if let Some(in_flight) = self.state_requests.remove(&commitment) {
434            in_flight.handle.abort();
435        }
436        self.start_pending(commitment);
437    }
438
439    /// Get the next output.
440    pub async fn next(&mut self) -> Option<StateManagerOutput<T>> {
441        loop {
442            match self.tasks.join_next().await {
443                Some(Ok(result)) => match result {
444                    Completed::State {
445                        response,
446                        leaf: leaf2,
447                    } => {
448                        if self.state_requests.remove(&response.commitment).is_none() {
449                            continue;
450                        }
451                        let decided = self
452                            .decided_in_flight
453                            .take_if(|leaf| leaf.commit() == response.commitment);
454                        if let Some(leaf) = leaf2 {
455                            let measurement = self
456                                .update_leaf_duration_metric
457                                .clone()
458                                .map(Measurement::start);
459                            self.insert_state(
460                                response.view,
461                                response.state.clone(),
462                                response.delta.clone(),
463                                leaf,
464                            );
465                            finish_measurement(measurement);
466                            self.start_pending(response.commitment);
467                            return Some(StateManagerOutput::State {
468                                response,
469                                validated: true,
470                            });
471                        } else {
472                            if self.validated_states.contains_key(&response.commitment) {
473                                self.start_pending(response.commitment);
474                            } else if let Some(leaf) = decided {
475                                self.seed_decided(leaf);
476                            }
477                            return Some(StateManagerOutput::State {
478                                response,
479                                validated: false,
480                            });
481                        }
482                    },
483                    Completed::Header { response, header } => {
484                        let key = (
485                            response.view,
486                            proposal_commitment(&response.parent_proposal),
487                        );
488                        if self.header_requests.remove(&key).is_none() {
489                            continue;
490                        }
491                        return Some(StateManagerOutput::Header { response, header });
492                    },
493                },
494                Some(Err(err)) => {
495                    if err.is_panic() {
496                        error!(%err, "task panicked");
497                    }
498                },
499                None => return None,
500            }
501        }
502    }
503
504    /// The decided view's own validation, or a header this node needs to
505    /// propose, may be queued behind a validation aborted here. A stub for the
506    /// aborted proposal lets them proceed via catchup instead of hanging.
507    pub fn gc(&mut self, view_number: ViewNumber) {
508        let margin_view = ViewNumber::new(view_number.saturating_sub(STATE_GC_MARGIN));
509        self.validated_states.retain(|_, entry| {
510            let view = entry.leaf.view_number();
511            view >= view_number || (entry.delta.is_some() && view >= margin_view)
512        });
513
514        self.decided_in_flight
515            .take_if(|leaf| leaf.view_number() < view_number);
516
517        self.header_requests.retain(|(view, _), handle| {
518            let keep = *view >= view_number;
519            if !keep {
520                handle.abort();
521            }
522            keep
523        });
524
525        self.pending_requests.retain(|_, pending| {
526            pending.retain(|p| p.view() >= view_number);
527            !pending.is_empty()
528        });
529
530        let stale: Vec<_> = self
531            .state_requests
532            .extract_if(|_, in_flight| in_flight.view < view_number)
533            .collect();
534        for (commitment, in_flight) in stale {
535            in_flight.handle.abort();
536            if self.pending_requests.contains_key(&commitment) {
537                self.seed_from_header(in_flight.proposal);
538            }
539        }
540    }
541
542    fn start_pending(&mut self, finished_commitment: Commitment<Leaf2<T>>) {
543        let Some(pending) = self.pending_requests.remove(&finished_commitment) else {
544            return;
545        };
546        for p in pending {
547            match p {
548                Pending::State(r) => self.request_state(r),
549                Pending::Header(r) => self.request_header(r),
550            }
551        }
552    }
553
554    /// Insert a state into the validated states map.
555    ///
556    /// States created via `from_header`
557    /// have no delta. States produced by `validate_and_apply_header` carry a delta representing
558    /// the state transition. This method prevents a `from_header` state from overwriting a
559    /// fully validated state that already has a delta.
560    fn insert_state(
561        &mut self,
562        view: ViewNumber,
563        state: Arc<T::ValidatedState>,
564        delta: Option<Delta<T>>,
565        leaf: Leaf2<T>,
566    ) {
567        if let Some(existing) = self.validated_states.get(&leaf.commit())
568            && existing.delta.is_some()
569            && delta.is_none()
570        {
571            warn!(
572                ?view,
573                "Skipping state update to not override a state with a delta"
574            );
575            return;
576        }
577        self.validated_states
578            .insert(leaf.commit(), StateEntry { state, delta, leaf });
579    }
580
581    fn insert_empty_state(&mut self, proposal: Proposal<T>) {
582        let state = T::ValidatedState::from_header(&proposal.block_header);
583        self.insert_state(
584            proposal.view_number(),
585            Arc::new(state),
586            None,
587            proposal.into(),
588        );
589    }
590
591    #[cfg(test)]
592    pub(crate) fn validated_contains_view(&self, v: ViewNumber) -> bool {
593        self.validated_states
594            .iter()
595            .any(|(_, entry)| entry.leaf.view_number() == v)
596    }
597}