Skip to main content

espresso_node/
api.rs

1use std::{collections::HashMap, pin::Pin, sync::Arc, time::Duration};
2
3use ::light_client::{
4    LightClient,
5    client::{FallbackClient, QueryServiceClient},
6    state::{Genesis, LightClientOptions},
7    storage::{LightClientSqliteOptions, SqliteStorage},
8};
9use alloy::primitives::U256;
10use anyhow::{Context, bail, ensure};
11use async_lock::RwLock;
12use async_once_cell::Lazy;
13use async_trait::async_trait;
14use committable::Commitment;
15use data_source::{
16    CatchupDataSource, RequestResponseDataSource, StakeTableDataSource, StakeTableWithEpochNumber,
17    StateCertDataSource, StateCertFetchingDataSource, SubmitDataSource,
18};
19use derivative::Derivative;
20use espresso_types::{
21    AccountQueryData, AuthenticatedValidatorMap, BlockMerkleTree, FeeAccount, FeeMerkleTree, Leaf2,
22    NodeState, PubKey, Transaction,
23    config::PublicNetworkConfig,
24    retain_accounts,
25    traits::EventsPersistenceRead,
26    v0::traits::{SequencerPersistence, StateCatchup},
27    v0_3::{
28        ChainConfig, RegisteredValidator, RewardAccountQueryDataV1, RewardAccountV1, RewardAmount,
29        RewardMerkleTreeV1, StakeTableEvent,
30    },
31    v0_4::{
32        PermittedRewardMerkleTreeV2, RewardAccountQueryDataV2, RewardAccountV2, RewardMerkleTreeV2,
33    },
34};
35use futures::{
36    future::{BoxFuture, Future, FutureExt},
37    stream::BoxStream,
38};
39use hotshot_contract_adapter::sol_types::EspToken;
40use hotshot_events_service::events_source::{
41    EventFilterSet, EventsSource, EventsStreamer, StartupInfo,
42};
43use hotshot_query_service::{
44    availability::VidCommonQueryData,
45    data_source::ExtensibleDataSource,
46    fetching::{self, Provider},
47};
48use hotshot_types::{
49    PeerConfig,
50    data::{EpochNumber, VidCommitment, VidCommon, VidShare, ViewNumber},
51    event::{Event, LegacyEvent},
52    light_client::LCV3StateSignatureRequestBody,
53    network::NetworkConfig,
54    simple_certificate::LightClientStateUpdateCertificateV2,
55    stake_table::HSStakeTable,
56    traits::{
57        election::{Membership, MembershipSnapshot, NonEpochMembershipSnapshot},
58        network::ConnectedNetwork,
59    },
60    utils::epoch_from_block_number,
61    vid::avidm::{AvidMScheme, init_avidm_param},
62    vote::HasViewNumber,
63};
64use itertools::Itertools;
65use jf_merkle_tree_compat::MerkleTreeScheme;
66use moka::future::Cache;
67use rand::Rng;
68use request_response::RequestType;
69use serde::{Deserialize, Serialize};
70use tokio::time::timeout;
71use url::Url;
72use vbs::version::Version;
73
74use self::data_source::{
75    HotShotConfigDataSource, NodeKeysDataSource, NodePublicKeys, NodeStateDataSource,
76    StateSignatureDataSource,
77};
78use crate::{
79    SeqTypes, SequencerApiVersion, SequencerContext,
80    api::data_source::TokenDataSource,
81    catchup::{
82        CatchupStorage, add_fee_accounts_to_state, add_v1_reward_accounts_to_state,
83        add_v2_reward_accounts_to_state,
84    },
85    consensus_handle::ConsensusHandle,
86    context::ConsensusNode,
87    request_response::{
88        data_source::{retain_v1_reward_accounts, retain_v2_reward_accounts},
89        request::{Request, Response},
90    },
91    state_cert::{StateCertFetchError, validate_state_cert},
92    state_signature::StateSigner,
93};
94
95pub mod data_source;
96pub mod fs;
97pub mod light_client;
98pub mod options;
99pub mod sql;
100pub mod state;
101pub mod unlock_schedule;
102mod update;
103
104pub use options::Options;
105
106pub type BlocksFrontier = <BlockMerkleTree as MerkleTreeScheme>::MembershipProof;
107
108type BoxLazy<T> = Pin<Arc<Lazy<T, BoxFuture<'static, T>>>>;
109
110#[derive(Derivative)]
111#[derivative(Clone(bound = ""), Debug(bound = ""))]
112struct ApiState<N: ConnectedNetwork<PubKey>, P: SequencerPersistence> {
113    // The consensus state is initialized lazily so we can start the API (and healthcheck endpoints)
114    // before consensus has started. Any endpoint that uses consensus state will wait for
115    // initialization to finish, but endpoints that do not require a consensus handle can proceed
116    // without waiting.
117    #[derivative(Debug = "ignore")]
118    sequencer_context: BoxLazy<SequencerContext<N, P>>,
119
120    // we cache `token_supply` for up to an hour, to avoid repeatedly querying the contract for information that rarely changes
121    token_supply: Cache<(), U256>,
122}
123
124impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence> ApiState<N, P> {
125    fn new(context_init: impl Future<Output = SequencerContext<N, P>> + Send + 'static) -> Self {
126        Self {
127            sequencer_context: Arc::pin(Lazy::from_future(context_init.boxed())),
128            token_supply: Cache::builder()
129                .max_capacity(1)
130                .time_to_live(Duration::from_secs(3600))
131                .build(),
132        }
133    }
134
135    async fn state_signer(&self) -> Arc<RwLock<StateSigner<SequencerApiVersion>>> {
136        self.sequencer_context
137            .as_ref()
138            .get()
139            .await
140            .get_ref()
141            .state_signer()
142    }
143
144    async fn event_streamer(&self) -> Arc<RwLock<EventsStreamer<SeqTypes>>> {
145        self.sequencer_context
146            .as_ref()
147            .get()
148            .await
149            .get_ref()
150            .event_streamer()
151    }
152
153    async fn consensus_handle(&self) -> Arc<ConsensusHandle<SeqTypes, ConsensusNode<N, P>>> {
154        self.sequencer_context
155            .as_ref()
156            .get()
157            .await
158            .get_ref()
159            .consensus_handle()
160    }
161
162    async fn network_config(&self) -> NetworkConfig<SeqTypes> {
163        self.sequencer_context
164            .as_ref()
165            .get()
166            .await
167            .get_ref()
168            .network_config()
169    }
170}
171
172type StorageState<N, P, D> = ExtensibleDataSource<D, ApiState<N, P>>;
173
174#[async_trait]
175impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence> EventsSource<SeqTypes>
176    for ApiState<N, P>
177{
178    type EventStream = BoxStream<'static, Arc<Event<SeqTypes>>>;
179    type LegacyEventStream = BoxStream<'static, Arc<LegacyEvent<SeqTypes>>>;
180
181    async fn get_event_stream(
182        &self,
183        _filter: Option<EventFilterSet<SeqTypes>>,
184    ) -> Self::EventStream {
185        self.event_streamer()
186            .await
187            .read()
188            .await
189            .get_event_stream(None)
190            .await
191    }
192
193    async fn get_legacy_event_stream(
194        &self,
195        _filter: Option<EventFilterSet<SeqTypes>>,
196    ) -> Self::LegacyEventStream {
197        self.event_streamer()
198            .await
199            .read()
200            .await
201            .get_legacy_event_stream(None)
202            .await
203    }
204
205    async fn get_startup_info(&self) -> StartupInfo<SeqTypes> {
206        self.event_streamer()
207            .await
208            .read()
209            .await
210            .get_startup_info()
211            .await
212    }
213}
214
215impl<N: ConnectedNetwork<PubKey>, D: Send + Sync, P: SequencerPersistence> TokenDataSource<SeqTypes>
216    for StorageState<N, P, D>
217{
218    async fn get_initial_supply_l1(&self) -> anyhow::Result<U256> {
219        self.as_ref().get_initial_supply_l1().await
220    }
221
222    async fn get_total_supply_l1(&self) -> anyhow::Result<U256> {
223        self.as_ref().get_total_supply_l1().await
224    }
225
226    async fn get_decided_header(&self) -> espresso_types::Header {
227        self.as_ref().get_decided_header().await
228    }
229}
230
231impl<N: ConnectedNetwork<PubKey>, D: Send + Sync, P: SequencerPersistence> SubmitDataSource<N, P>
232    for StorageState<N, P, D>
233{
234    async fn submit(&self, tx: Transaction) -> anyhow::Result<()> {
235        self.as_ref().submit(tx).await
236    }
237}
238
239impl<N: ConnectedNetwork<PubKey>, D: Sync, P: SequencerPersistence> StakeTableDataSource<SeqTypes>
240    for StorageState<N, P, D>
241{
242    /// Get the stake table for a given epoch
243    async fn get_stake_table(
244        &self,
245        epoch: Option<EpochNumber>,
246    ) -> anyhow::Result<Vec<PeerConfig<SeqTypes>>> {
247        self.as_ref().get_stake_table(epoch).await
248    }
249
250    /// Get the stake table for the current epoch if not provided
251    async fn get_stake_table_current(&self) -> anyhow::Result<StakeTableWithEpochNumber<SeqTypes>> {
252        self.as_ref().get_stake_table_current().await
253    }
254
255    /// Get the DA stake table for a given epoch
256    async fn get_da_stake_table(
257        &self,
258        epoch: Option<EpochNumber>,
259    ) -> anyhow::Result<Vec<PeerConfig<SeqTypes>>> {
260        self.as_ref().get_da_stake_table(epoch).await
261    }
262
263    /// Get the DA stake table for the current epoch if not provided
264    async fn get_da_stake_table_current(
265        &self,
266    ) -> anyhow::Result<StakeTableWithEpochNumber<SeqTypes>> {
267        self.as_ref().get_da_stake_table_current().await
268    }
269
270    /// Get all the validators
271    async fn get_validators(
272        &self,
273        epoch: EpochNumber,
274    ) -> anyhow::Result<AuthenticatedValidatorMap> {
275        self.as_ref().get_validators(epoch).await
276    }
277
278    async fn get_block_reward(
279        &self,
280        epoch: Option<EpochNumber>,
281    ) -> anyhow::Result<Option<RewardAmount>> {
282        self.as_ref().get_block_reward(epoch).await
283    }
284    /// Get all the validator participation for the current epoch
285    async fn current_proposal_participation(&self) -> HashMap<PubKey, f64> {
286        self.as_ref().current_proposal_participation().await
287    }
288    /// Get all the validator participation for the previous epoch
289    async fn proposal_participation(&self, epoch: EpochNumber) -> HashMap<PubKey, f64> {
290        self.as_ref().proposal_participation(epoch).await
291    }
292    /// Get all the vote participation for the current epoch
293    async fn current_vote_participation(&self) -> HashMap<PubKey, f64> {
294        self.as_ref().current_vote_participation().await
295    }
296    /// Get all the vote participation for a given epoch
297    async fn vote_participation(&self, epoch: EpochNumber) -> HashMap<PubKey, f64> {
298        self.as_ref().vote_participation(epoch).await
299    }
300
301    async fn get_all_validators(
302        &self,
303        epoch: EpochNumber,
304        offset: u64,
305        limit: u64,
306    ) -> anyhow::Result<Vec<RegisteredValidator<PubKey>>> {
307        self.as_ref().get_all_validators(epoch, offset, limit).await
308    }
309
310    async fn stake_table_events(
311        &self,
312        from_l1_block: u64,
313        to_l1_block: u64,
314    ) -> anyhow::Result<Vec<StakeTableEvent>> {
315        self.as_ref()
316            .stake_table_events(from_l1_block, to_l1_block)
317            .await
318    }
319}
320
321impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence> TokenDataSource<SeqTypes>
322    for ApiState<N, P>
323{
324    async fn get_initial_supply_l1(&self) -> anyhow::Result<U256> {
325        let node_state = self.sequencer_context.as_ref().get().await.node_state();
326        let fetcher = node_state.coordinator.membership().fetcher().clone();
327        Ok(fetcher.initial_supply_or_fetch().await?)
328    }
329
330    async fn get_total_supply_l1(&self) -> anyhow::Result<U256> {
331        match self.token_supply.get(&()).await {
332            Some(supply) => Ok(supply),
333            None => {
334                let node_state = self.sequencer_context.as_ref().get().await.node_state();
335                let token_contract_address = node_state.token_contract_address().await?;
336
337                let provider = node_state.l1_client.provider;
338
339                let token = EspToken::new(token_contract_address, provider.clone());
340
341                let supply = token
342                    .totalSupply()
343                    .call()
344                    .await
345                    .context("Failed to retrieve totalSupply from the contract")?;
346
347                self.token_supply.insert((), supply).await;
348
349                Ok(supply)
350            },
351        }
352    }
353
354    async fn get_decided_header(&self) -> espresso_types::Header {
355        self.consensus_handle()
356            .await
357            .decided_leaf()
358            .await
359            .block_header()
360            .clone()
361    }
362}
363
364impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence> StakeTableDataSource<SeqTypes>
365    for ApiState<N, P>
366{
367    /// Get the stake table for a given epoch
368    async fn get_stake_table(
369        &self,
370        epoch: Option<EpochNumber>,
371    ) -> anyhow::Result<Vec<PeerConfig<SeqTypes>>> {
372        let handle = self.consensus_handle().await;
373        if let Some(requested) = epoch {
374            let first_epoch = handle
375                .membership_coordinator()
376                .await
377                .membership()
378                .first_epoch();
379            if let Some(first_epoch) = first_epoch
380                && requested < first_epoch
381            {
382                return Err(anyhow::anyhow!(
383                    "requested stake table for epoch {requested:?} is below the first epoch \
384                     {first_epoch:?}"
385                ));
386            }
387        }
388        let highest_epoch = handle.current_epoch().await.map(|e| e + 1);
389        if epoch > highest_epoch {
390            return Err(anyhow::anyhow!(
391                "requested stake table for epoch {epoch:?} is beyond the current epoch + 1 \
392                 {highest_epoch:?}"
393            ));
394        }
395        let mem = handle
396            .membership_coordinator()
397            .await
398            .stake_table_for_epoch(epoch)?;
399
400        Ok(mem.stake_table().cloned().collect())
401    }
402
403    /// Get the stake table for the current epoch and return it along with the epoch number
404    async fn get_stake_table_current(&self) -> anyhow::Result<StakeTableWithEpochNumber<SeqTypes>> {
405        let epoch = self.consensus_handle().await.current_epoch().await;
406
407        Ok(StakeTableWithEpochNumber {
408            epoch,
409            stake_table: self.get_stake_table(epoch).await?,
410        })
411    }
412
413    /// Get the DA stake table for a given epoch
414    async fn get_da_stake_table(
415        &self,
416        epoch: Option<EpochNumber>,
417    ) -> anyhow::Result<Vec<PeerConfig<SeqTypes>>> {
418        let coordinator = self.consensus_handle().await.membership_coordinator().await;
419        Ok(match epoch {
420            Some(e) => coordinator
421                .membership()
422                .snapshot(e)
423                .map(|s| s.da_stake_table().cloned().collect())
424                .unwrap_or_default(),
425            None => coordinator
426                .membership()
427                .non_epoch_snapshot()
428                .da_stake_table()
429                .cloned()
430                .collect(),
431        })
432    }
433
434    /// Get the DA stake table for the current epoch and return it along with the epoch number
435    async fn get_da_stake_table_current(
436        &self,
437    ) -> anyhow::Result<StakeTableWithEpochNumber<SeqTypes>> {
438        let epoch = self.consensus_handle().await.current_epoch().await;
439
440        Ok(StakeTableWithEpochNumber {
441            epoch,
442            stake_table: self.get_da_stake_table(epoch).await?,
443        })
444    }
445
446    async fn get_block_reward(
447        &self,
448        epoch: Option<EpochNumber>,
449    ) -> anyhow::Result<Option<RewardAmount>> {
450        let coordinator = self.consensus_handle().await.membership_coordinator().await;
451
452        let membership = coordinator.membership();
453        let block_reward = match epoch {
454            None => membership.fixed_block_reward(),
455            Some(e) => membership.epoch_block_reward(e),
456        };
457
458        Ok(block_reward)
459    }
460
461    /// Get the whole validators map
462    async fn get_validators(&self, e: EpochNumber) -> anyhow::Result<AuthenticatedValidatorMap> {
463        Ok(self
464            .consensus_handle()
465            .await
466            .membership_coordinator()
467            .await
468            .membership_for_epoch(Some(e))
469            .context("membership not found")?
470            .snapshot()
471            .with_context(|| format!("no committee for epoch={e}"))?
472            .validators()
473            .clone())
474    }
475
476    /// Get the current proposal participation.
477    async fn current_proposal_participation(&self) -> HashMap<PubKey, f64> {
478        self.consensus_handle()
479            .await
480            .current_proposal_participation()
481            .await
482    }
483
484    /// Get the proposal participation for a given epoch.
485    async fn proposal_participation(&self, epoch: EpochNumber) -> HashMap<PubKey, f64> {
486        self.consensus_handle()
487            .await
488            .proposal_participation(epoch)
489            .await
490    }
491
492    /// Get the current vote participation.
493    async fn current_vote_participation(&self) -> HashMap<PubKey, f64> {
494        self.consensus_handle()
495            .await
496            .current_vote_participation()
497            .await
498    }
499
500    /// Get the vote participation for a given epoch.
501    async fn vote_participation(&self, epoch: EpochNumber) -> HashMap<PubKey, f64> {
502        self.consensus_handle()
503            .await
504            .vote_participation(epoch)
505            .await
506    }
507
508    async fn get_all_validators(
509        &self,
510        epoch: EpochNumber,
511        offset: u64,
512        limit: u64,
513    ) -> anyhow::Result<Vec<RegisteredValidator<PubKey>>> {
514        let storage = self.consensus_handle().await.storage().await;
515        storage.load_all_validators(epoch, offset, limit).await
516    }
517
518    async fn stake_table_events(
519        &self,
520        from_l1_block: u64,
521        to_l1_block: u64,
522    ) -> anyhow::Result<Vec<StakeTableEvent>> {
523        let storage = self.consensus_handle().await.storage().await;
524        let (status, events) = storage.load_events(from_l1_block, to_l1_block).await?;
525        ensure!(
526            status == Some(EventsPersistenceRead::Complete),
527            "some events in range [{from_l1_block}, {to_l1_block}] are not available ({status:?})"
528        );
529        Ok(events.into_iter().map(|(_, event)| event).collect())
530    }
531}
532
533impl<N: ConnectedNetwork<PubKey>, D: Sync, P: SequencerPersistence>
534    RequestResponseDataSource<SeqTypes> for StorageState<N, P, D>
535{
536    async fn request_vid_shares(
537        &self,
538        block_number: u64,
539        vid_common_data: VidCommonQueryData<SeqTypes>,
540        timeout_duration: Duration,
541    ) -> BoxFuture<'static, anyhow::Result<Vec<VidShare>>> {
542        self.as_ref()
543            .request_vid_shares(block_number, vid_common_data, timeout_duration)
544            .await
545    }
546}
547
548#[async_trait]
549impl<N: ConnectedNetwork<PubKey>, D: Sync, P: SequencerPersistence>
550    StateCertFetchingDataSource<SeqTypes> for StorageState<N, P, D>
551{
552    async fn request_state_cert(
553        &self,
554        epoch: u64,
555        timeout: Duration,
556    ) -> Result<LightClientStateUpdateCertificateV2<SeqTypes>, StateCertFetchError> {
557        self.as_ref().request_state_cert(epoch, timeout).await
558    }
559}
560
561impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence> RequestResponseDataSource<SeqTypes>
562    for ApiState<N, P>
563{
564    async fn request_vid_shares(
565        &self,
566        block_number: u64,
567        vid_common_data: VidCommonQueryData<SeqTypes>,
568        duration: Duration,
569    ) -> BoxFuture<'static, anyhow::Result<Vec<VidShare>>> {
570        // Get a handle to the request response protocol
571        let request_response_protocol = self
572            .sequencer_context
573            .as_ref()
574            .get()
575            .await
576            .request_response_protocol
577            .clone();
578
579        async move {
580            // Get the total VID weight based on the VID common data
581            let total_weight = match vid_common_data.common() {
582                VidCommon::V0(_) => {
583                    // TODO: This needs to be done via the stake table
584                    return Err(anyhow::anyhow!(
585                        "V0 total weight calculation not supported yet"
586                    ));
587                },
588                VidCommon::V1(v1) => v1.total_weights,
589                VidCommon::V2(v2) => v2.param.total_weights,
590            };
591
592            // Create the AvidM parameters from the total weight
593            let avidm_param = init_avidm_param(total_weight)
594                .with_context(|| "failed to initialize avidm param")?;
595
596            // Get the payload hash for verification
597            let VidCommitment::V1(local_payload_hash) = vid_common_data.payload_hash() else {
598                bail!("V0 share verification not supported yet");
599            };
600
601            // Create a random request id
602            let request_id = rand::thread_rng().r#gen();
603
604            // Request and verify the shares from all other nodes, timing out after `duration` seconds
605            let received_shares = Arc::new(parking_lot::Mutex::new(Vec::new()));
606            let received_shares_clone = received_shares.clone();
607            let request_result: anyhow::Result<_, _> = timeout(
608                duration,
609                request_response_protocol.request_indefinitely::<_, _, _>(
610                    Request::VidShare(block_number, request_id),
611                    RequestType::Batched,
612                    move |_request, response| {
613                        let avidm_param = avidm_param.clone();
614                        let received_shares = received_shares_clone.clone();
615                        async move {
616                            // Make sure the response was a V1 share
617                            let Response::VidShare(VidShare::V1(received_share)) = response else {
618                                bail!("V0 share verification not supported yet");
619                            };
620
621                            // Verify the share
622                            let Ok(Ok(_)) = AvidMScheme::verify_share(
623                                &avidm_param,
624                                &local_payload_hash,
625                                &received_share,
626                            ) else {
627                                bail!("share verification failed");
628                            };
629
630                            // Add the share to the list of received shares
631                            received_shares.lock().push(received_share);
632
633                            bail!("waiting for more shares");
634
635                            #[allow(unreachable_code)]
636                            Ok(())
637                        }
638                    },
639                ),
640            )
641            .await;
642
643            // If the request timed out, return the shares we have collected so far
644            match request_result {
645                Err(_) => {
646                    // If it timed out, this was successful. Return the shares we have collected so far
647                    Ok(received_shares
648                        .lock()
649                        .clone()
650                        .into_iter()
651                        .map(VidShare::V1)
652                        .collect())
653                },
654
655                // If it was an error from the inner request, return that error
656                Ok(Err(e)) => Err(e).with_context(|| "failed to request vid shares"),
657
658                // If it was successful, this was unexpected.
659                Ok(Ok(_)) => bail!("this should not be possible"),
660            }
661        }
662        .boxed()
663    }
664}
665
666#[async_trait]
667impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence> StateCertFetchingDataSource<SeqTypes>
668    for ApiState<N, P>
669{
670    async fn request_state_cert(
671        &self,
672        epoch: u64,
673        timeout: Duration,
674    ) -> Result<LightClientStateUpdateCertificateV2<SeqTypes>, StateCertFetchError> {
675        tracing::info!("fetching state certificate for epoch={epoch}");
676        let handle = self.consensus_handle().await;
677
678        let current_epoch = handle.current_epoch().await;
679
680        // The highest epoch we can have a state certificate for is current_epoch + 1
681        // Check if requested epoch is beyond the highest possible epoch
682        let highest_epoch = current_epoch.map(|e| e.u64() + 1);
683
684        if Some(epoch) > highest_epoch {
685            return Err(StateCertFetchError::Other(anyhow::anyhow!(
686                "requested state certificate for epoch {epoch} is beyond the highest possible \
687                 epoch {highest_epoch:?}"
688            )));
689        }
690
691        // Get the stake table for validation
692        let coordinator = handle.membership_coordinator().await;
693        if let Err(err) = coordinator.stake_table_for_epoch(Some(EpochNumber::new(epoch))) {
694            tracing::warn!(
695                "Failed to get membership for epoch {epoch}: {err:#}. Waiting for catchup"
696            );
697
698            coordinator
699                .wait_for_catchup(EpochNumber::new(epoch))
700                .await
701                .map_err(|e| {
702                    StateCertFetchError::Other(
703                        anyhow::Error::new(e)
704                            .context(format!("failed to catch up for stake table epoch={epoch}")),
705                    )
706                })?;
707        }
708
709        let membership = coordinator
710            .stake_table_for_epoch(Some(EpochNumber::new(epoch)))
711            .map_err(|e| {
712                StateCertFetchError::Other(
713                    anyhow::Error::new(e)
714                        .context(format!("failed to get stake table for epoch={epoch}")),
715                )
716            })?;
717
718        let stake_table = HSStakeTable::from_iter(membership.stake_table());
719
720        let state_catchup = self
721            .sequencer_context
722            .as_ref()
723            .get()
724            .await
725            .node_state()
726            .state_catchup
727            .clone();
728
729        let result = tokio::time::timeout(timeout, state_catchup.fetch_state_cert(epoch)).await;
730
731        match result {
732            Err(_) => Err(StateCertFetchError::FetchError(anyhow::anyhow!(
733                "timeout while fetching state cert for epoch {epoch}"
734            ))),
735            Ok(Ok(cert)) => {
736                // Validation errors should be mapped to ValidationError
737                validate_state_cert(
738                    &cert,
739                    &stake_table,
740                    EpochNumber::new(epoch),
741                    *coordinator.epoch_height(),
742                    &handle.upgrade_lock().await,
743                )
744                .map_err(|e| {
745                    StateCertFetchError::ValidationError(e.context(format!(
746                        "state certificate validation failed for epoch={epoch}"
747                    )))
748                })?;
749
750                tracing::info!("fetched and validated state certificate for epoch {epoch}");
751                Ok(cert)
752            },
753            Ok(Err(e)) => Err(StateCertFetchError::FetchError(
754                e.context(format!("failed to fetch state cert for epoch {epoch}")),
755            )),
756        }
757    }
758}
759
760// Thin wrapper implementations that delegate to persistence
761#[async_trait]
762impl<N: ConnectedNetwork<PubKey>, D: Sync, P: SequencerPersistence> StateCertDataSource
763    for StorageState<N, P, D>
764{
765    async fn get_state_cert_by_epoch(
766        &self,
767        epoch: u64,
768    ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
769        self.as_ref().get_state_cert_by_epoch(epoch).await
770    }
771
772    async fn insert_state_cert(
773        &self,
774        epoch: u64,
775        cert: LightClientStateUpdateCertificateV2<SeqTypes>,
776    ) -> anyhow::Result<()> {
777        self.as_ref().insert_state_cert(epoch, cert).await
778    }
779}
780
781#[async_trait]
782impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence> StateCertDataSource for ApiState<N, P> {
783    async fn get_state_cert_by_epoch(
784        &self,
785        epoch: u64,
786    ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
787        let storage = self.consensus_handle().await.storage().await;
788        storage.get_state_cert_by_epoch(epoch).await
789    }
790
791    async fn insert_state_cert(
792        &self,
793        epoch: u64,
794        cert: LightClientStateUpdateCertificateV2<SeqTypes>,
795    ) -> anyhow::Result<()> {
796        let storage = self.consensus_handle().await.storage().await;
797        storage.insert_state_cert(epoch, cert).await
798    }
799}
800
801impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence> SubmitDataSource<N, P>
802    for ApiState<N, P>
803{
804    async fn submit(&self, tx: Transaction) -> anyhow::Result<()> {
805        let handle = self.consensus_handle().await;
806
807        // Fetch full chain config from the validated state, if present.
808        // This is necessary because we support chain config upgrades,
809        // so the updated chain config is found in the validated state.
810        let cf = handle
811            .decided_state()
812            .await
813            .and_then(|state| state.chain_config.resolve());
814
815        // Use the chain config from the validated state if available,
816        // otherwise, use the node state's chain config
817        // The node state's chain config is the node's base version chain config
818        let cf = match cf {
819            Some(cf) => cf,
820            None => self.node_state().await.chain_config,
821        };
822
823        let max_block_size: u64 = cf.max_block_size.into();
824        let txn_size = tx.payload().len() as u64;
825
826        // reject transaction bigger than block size
827        if txn_size > max_block_size {
828            bail!("transaction size ({txn_size}) is greater than max_block_size ({max_block_size})")
829        }
830
831        handle.submit_transaction(tx).await?;
832        Ok(())
833    }
834}
835
836impl<N, P, D> NodeStateDataSource for StorageState<N, P, D>
837where
838    N: ConnectedNetwork<PubKey>,
839    P: SequencerPersistence,
840    D: Sync,
841{
842    async fn node_state(&self) -> NodeState {
843        self.as_ref().node_state().await
844    }
845}
846
847impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence, D: CatchupStorage + Send + Sync>
848    data_source::DatabaseMetadataSource for StorageState<N, P, D>
849where
850    N: ConnectedNetwork<PubKey>,
851    P: SequencerPersistence,
852    D: data_source::DatabaseMetadataSource + Send + Sync,
853{
854    async fn get_table_sizes(&self) -> anyhow::Result<Vec<data_source::TableSize>> {
855        self.inner().get_table_sizes().await
856    }
857
858    async fn get_migration_status(&self) -> anyhow::Result<Vec<data_source::MigrationStatus>> {
859        self.inner().get_migration_status().await
860    }
861}
862
863impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence, D: CatchupStorage + Send + Sync>
864    data_source::PruningDataSource for StorageState<N, P, D>
865where
866    N: ConnectedNetwork<PubKey>,
867    P: SequencerPersistence,
868    D: data_source::PruningDataSource + Send + Sync,
869{
870    async fn get_oldest_block(
871        &self,
872    ) -> anyhow::Result<Option<hotshot_query_service::availability::BlockQueryData<crate::SeqTypes>>>
873    {
874        self.inner().get_oldest_block().await
875    }
876
877    async fn get_oldest_leaf(
878        &self,
879    ) -> anyhow::Result<Option<hotshot_query_service::availability::LeafQueryData<crate::SeqTypes>>>
880    {
881        self.inner().get_oldest_leaf().await
882    }
883}
884
885impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence, D: CatchupStorage + Send + Sync>
886    CatchupDataSource for StorageState<N, P, D>
887{
888    #[tracing::instrument(skip(self, instance))]
889    async fn get_accounts(
890        &self,
891        instance: &NodeState,
892        height: u64,
893        view: ViewNumber,
894        accounts: &[FeeAccount],
895    ) -> anyhow::Result<FeeMerkleTree> {
896        // Check if we have the desired state in memory.
897        match self
898            .as_ref()
899            .get_accounts(instance, height, view, accounts)
900            .await
901        {
902            Ok(accounts) => return Ok(accounts),
903            Err(err) => {
904                tracing::info!("accounts not in memory, trying storage: {err:#}");
905            },
906        }
907
908        // Try storage.
909        let (tree, leaf) = self
910            .inner()
911            .get_accounts(instance, height, view, accounts)
912            .await
913            .context("accounts not in memory, and could not fetch from storage")?;
914        // If we successfully fetched accounts from storage, try to add them back into the in-memory
915        // state.
916
917        let handle = self.as_ref().consensus_handle().await;
918        if let Err(err) = add_fee_accounts_to_state(&*handle, &view, accounts, &tree, leaf).await {
919            tracing::warn!(?view, "cannot update fetched account state: {err:#}");
920        }
921        tracing::info!(?view, "updated with fetched account state");
922
923        Ok(tree)
924    }
925
926    #[tracing::instrument(skip(self, instance))]
927    async fn get_frontier(
928        &self,
929        instance: &NodeState,
930        height: u64,
931        view: ViewNumber,
932    ) -> anyhow::Result<BlocksFrontier> {
933        // Check if we have the desired state in memory.
934        match self.as_ref().get_frontier(instance, height, view).await {
935            Ok(frontier) => return Ok(frontier),
936            Err(err) => {
937                tracing::info!("frontier is not in memory, trying storage: {err:#}");
938            },
939        }
940
941        // Try storage.
942        self.inner().get_frontier(instance, height, view).await
943    }
944
945    async fn get_chain_config(
946        &self,
947        commitment: Commitment<ChainConfig>,
948    ) -> anyhow::Result<ChainConfig> {
949        // Check if we have the desired state in memory.
950        match self.as_ref().get_chain_config(commitment).await {
951            Ok(cf) => return Ok(cf),
952            Err(err) => {
953                tracing::info!("chain config is not in memory, trying storage: {err:#}");
954            },
955        }
956
957        // Try storage.
958        self.inner().get_chain_config(commitment).await
959    }
960    async fn get_leaf_chain(&self, height: u64) -> anyhow::Result<Vec<Leaf2>> {
961        // Check if we have the desired state in memory.
962        match self.as_ref().get_leaf_chain(height).await {
963            Ok(cf) => return Ok(cf),
964            Err(err) => {
965                tracing::info!("leaf chain is not in memory, trying storage: {err:#}");
966            },
967        }
968
969        // Try storage.
970        self.inner().get_leaf_chain(height).await
971    }
972
973    async fn get_cert2(
974        &self,
975        height: u64,
976    ) -> anyhow::Result<Option<espresso_types::Certificate2<SeqTypes>>> {
977        self.inner().load_cert2(height).await
978    }
979
980    #[tracing::instrument(skip(self, instance))]
981    async fn get_reward_accounts_v2(
982        &self,
983        instance: &NodeState,
984        height: u64,
985        view: ViewNumber,
986        accounts: &[RewardAccountV2],
987    ) -> anyhow::Result<RewardMerkleTreeV2> {
988        // Check if we have the desired state in memory.
989        match self
990            .as_ref()
991            .get_reward_accounts_v2(instance, height, view, accounts)
992            .await
993        {
994            Ok(accounts) => return Ok(accounts),
995            Err(err) => {
996                tracing::info!("reward accounts not in memory, trying storage: {err:#}");
997            },
998        }
999
1000        // Try storage.
1001        let (tree, leaf) = self
1002            .inner()
1003            .get_reward_accounts_v2(instance, height, view, accounts)
1004            .await
1005            .context("accounts not in memory, and could not fetch from storage")?;
1006
1007        // If we successfully fetched accounts from storage, try to add them back into the in-memory
1008        // state.
1009        let handle = self.as_ref().consensus_handle().await;
1010        if let Err(err) =
1011            add_v2_reward_accounts_to_state(&*handle, &view, accounts, &tree, leaf).await
1012        {
1013            tracing::warn!(?view, "cannot update fetched account state: {err:#}");
1014        }
1015        tracing::info!(?view, "updated with fetched account state");
1016
1017        Ok(tree)
1018    }
1019
1020    #[tracing::instrument(skip(self, instance))]
1021    async fn get_reward_accounts_v1(
1022        &self,
1023        instance: &NodeState,
1024        height: u64,
1025        view: ViewNumber,
1026        accounts: &[RewardAccountV1],
1027    ) -> anyhow::Result<RewardMerkleTreeV1> {
1028        // Check if we have the desired state in memory.
1029        match self
1030            .as_ref()
1031            .get_reward_accounts_v1(instance, height, view, accounts)
1032            .await
1033        {
1034            Ok(accounts) => return Ok(accounts),
1035            Err(err) => {
1036                tracing::info!("reward accounts not in memory, trying storage: {err:#}");
1037            },
1038        }
1039
1040        // Try storage.
1041        let (tree, leaf) = self
1042            .inner()
1043            .get_reward_accounts_v1(instance, height, view, accounts)
1044            .await
1045            .context("accounts not in memory, and could not fetch from storage")?;
1046
1047        // If we successfully fetched accounts from storage, try to add them back into the in-memory
1048        // state.
1049        let handle = self.as_ref().consensus_handle().await;
1050        if let Err(err) =
1051            add_v1_reward_accounts_to_state(&*handle, &view, accounts, &tree, leaf).await
1052        {
1053            tracing::warn!(?view, "cannot update fetched account state: {err:#}");
1054        }
1055        tracing::info!(?view, "updated with fetched account state");
1056
1057        Ok(tree)
1058    }
1059
1060    async fn get_reward_merkle_tree_v2(
1061        &self,
1062        height: u64,
1063        view: ViewNumber,
1064    ) -> anyhow::Result<Vec<u8>> {
1065        self.as_ref().get_reward_merkle_tree_v2(height, view).await
1066    }
1067
1068    #[tracing::instrument(skip(self))]
1069    async fn get_state_cert(
1070        &self,
1071        epoch: u64,
1072    ) -> anyhow::Result<LightClientStateUpdateCertificateV2<SeqTypes>> {
1073        let storage = self.as_ref().consensus_handle().await.storage().await;
1074        storage
1075            .get_state_cert_by_epoch(epoch)
1076            .await?
1077            .context(format!("state cert for epoch {epoch} not found"))
1078    }
1079}
1080
1081impl<N, P> NodeStateDataSource for ApiState<N, P>
1082where
1083    N: ConnectedNetwork<PubKey>,
1084    P: SequencerPersistence,
1085{
1086    async fn node_state(&self) -> NodeState {
1087        self.sequencer_context.as_ref().get().await.node_state()
1088    }
1089}
1090
1091impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence> CatchupDataSource for ApiState<N, P> {
1092    #[tracing::instrument(skip(self, _instance))]
1093    async fn get_accounts(
1094        &self,
1095        _instance: &NodeState,
1096        height: u64,
1097        view: ViewNumber,
1098        accounts: &[FeeAccount],
1099    ) -> anyhow::Result<FeeMerkleTree> {
1100        let state = self
1101            .consensus_handle()
1102            .await
1103            .state(view)
1104            .await
1105            .context(format!(
1106                "state not available for height {height}, view {view}"
1107            ))?;
1108        retain_accounts(&state.fee_merkle_tree, accounts.iter().copied())
1109    }
1110
1111    #[tracing::instrument(skip(self, _instance))]
1112    async fn get_frontier(
1113        &self,
1114        _instance: &NodeState,
1115        height: u64,
1116        view: ViewNumber,
1117    ) -> anyhow::Result<BlocksFrontier> {
1118        let state = self
1119            .consensus_handle()
1120            .await
1121            .state(view)
1122            .await
1123            .context(format!(
1124                "state not available for height {height}, view {view}"
1125            ))?;
1126        let tree = &state.block_merkle_tree;
1127        let frontier = tree.lookup(tree.num_leaves() - 1).expect_ok()?.1;
1128        Ok(frontier)
1129    }
1130
1131    async fn get_chain_config(
1132        &self,
1133        commitment: Commitment<ChainConfig>,
1134    ) -> anyhow::Result<ChainConfig> {
1135        let state = self
1136            .consensus_handle()
1137            .await
1138            .decided_state()
1139            .await
1140            .context("decided state not available")?;
1141        let chain_config = state.chain_config;
1142
1143        if chain_config.commit() == commitment {
1144            chain_config.resolve().context("chain config found")
1145        } else {
1146            bail!("chain config not found")
1147        }
1148    }
1149
1150    async fn get_leaf_chain(&self, height: u64) -> anyhow::Result<Vec<Leaf2>> {
1151        // Builds a legacy 3-chain from undecided leaves in memory. New-protocol heights fall
1152        // through to the storage path.
1153        let mut leaves = self.consensus_handle().await.undecided_leaves().await;
1154        leaves.sort_by_key(|l| l.view_number());
1155        let (position, mut last_leaf) = leaves
1156            .iter()
1157            .find_position(|l| l.height() == height)
1158            .context(format!("leaf chain not available for {height}"))?;
1159        let mut chain = vec![last_leaf.clone()];
1160        for leaf in leaves.iter().skip(position + 1) {
1161            if leaf.justify_qc().view_number() == last_leaf.view_number() {
1162                chain.push(leaf.clone());
1163            } else {
1164                continue;
1165            }
1166            if leaf.view_number() == last_leaf.view_number() + 1 {
1167                // one away from decide
1168                last_leaf = leaf;
1169                break;
1170            }
1171            last_leaf = leaf;
1172        }
1173        // Make sure we got one more leaf to confirm the decide
1174        for leaf in leaves
1175            .iter()
1176            .skip_while(|l| l.view_number() <= last_leaf.view_number())
1177        {
1178            if leaf.justify_qc().view_number() == last_leaf.view_number() {
1179                chain.push(leaf.clone());
1180                return Ok(chain);
1181            }
1182        }
1183        bail!(format!("leaf chain not available for {height}"))
1184    }
1185
1186    #[tracing::instrument(skip(self, _instance))]
1187    async fn get_reward_accounts_v2(
1188        &self,
1189        _instance: &NodeState,
1190        height: u64,
1191        view: ViewNumber,
1192        accounts: &[RewardAccountV2],
1193    ) -> anyhow::Result<RewardMerkleTreeV2> {
1194        let state = self
1195            .consensus_handle()
1196            .await
1197            .state(view)
1198            .await
1199            .context(format!(
1200                "state not available for height {height}, view {view}"
1201            ))?;
1202
1203        retain_v2_reward_accounts(&state.reward_merkle_tree_v2, accounts.iter().copied())
1204    }
1205
1206    #[tracing::instrument(skip(self, _instance))]
1207    async fn get_reward_accounts_v1(
1208        &self,
1209        _instance: &NodeState,
1210        height: u64,
1211        view: ViewNumber,
1212        accounts: &[RewardAccountV1],
1213    ) -> anyhow::Result<RewardMerkleTreeV1> {
1214        let state = self
1215            .consensus_handle()
1216            .await
1217            .state(view)
1218            .await
1219            .context(format!(
1220                "state not available for height {height}, view {view}"
1221            ))?;
1222
1223        retain_v1_reward_accounts(&state.reward_merkle_tree_v1, accounts.iter().copied())
1224    }
1225
1226    async fn get_reward_merkle_tree_v2(
1227        &self,
1228        height: u64,
1229        view: ViewNumber,
1230    ) -> anyhow::Result<Vec<u8>> {
1231        let state = self
1232            .consensus_handle()
1233            .await
1234            .state(view)
1235            .await
1236            .context(format!(
1237                "state not available for height {height}, view {view}"
1238            ))?;
1239
1240        let tree_data = TryInto::<RewardMerkleTreeV2Data>::try_into(&state.reward_merkle_tree_v2)
1241            .inspect_err(
1242            |err| tracing::debug!(%err, height, %view, "cannot serve reward merkle tree"),
1243        )?;
1244        let merkle_tree_bytes = bincode::serialize(&tree_data)
1245            .context("Merkle tree serialization failed; this should never happen.")?;
1246
1247        Ok(merkle_tree_bytes)
1248    }
1249
1250    async fn get_state_cert(
1251        &self,
1252        epoch: u64,
1253    ) -> anyhow::Result<LightClientStateUpdateCertificateV2<SeqTypes>> {
1254        self.get_state_cert_by_epoch(epoch)
1255            .await?
1256            .context(format!("state cert not found for epoch {epoch}"))
1257    }
1258}
1259
1260impl<N: ConnectedNetwork<PubKey>, D: Sync, P: SequencerPersistence> HotShotConfigDataSource
1261    for StorageState<N, P, D>
1262{
1263    async fn get_config(&self) -> PublicNetworkConfig {
1264        self.as_ref().network_config().await.into()
1265    }
1266}
1267
1268impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence> HotShotConfigDataSource
1269    for ApiState<N, P>
1270{
1271    async fn get_config(&self) -> PublicNetworkConfig {
1272        self.network_config().await.into()
1273    }
1274}
1275
1276impl<N: ConnectedNetwork<PubKey>, D: Sync, P: SequencerPersistence> NodeKeysDataSource
1277    for StorageState<N, P, D>
1278{
1279    async fn node_public_keys(&self) -> NodePublicKeys {
1280        self.as_ref().node_public_keys().await
1281    }
1282}
1283
1284impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence> NodeKeysDataSource for ApiState<N, P> {
1285    async fn node_public_keys(&self) -> NodePublicKeys {
1286        let ctx = self.sequencer_context.as_ref().get().await.get_ref();
1287        let config = ctx.validator_config();
1288        let consensus_key = config.public_key;
1289        let eth_account = ctx
1290            .consensus_handle()
1291            .membership_coordinator()
1292            .await
1293            .membership()
1294            .latest_account(&consensus_key);
1295        NodePublicKeys {
1296            eth_account,
1297            consensus_key,
1298            state_ver_key: config.state_public_key.clone(),
1299            x25519_key: config.x25519_keypair.as_ref().map(|kp| kp.public_key()),
1300        }
1301    }
1302}
1303
1304#[async_trait]
1305impl<N: ConnectedNetwork<PubKey>, D: Sync, P: SequencerPersistence> StateSignatureDataSource<N>
1306    for StorageState<N, P, D>
1307{
1308    async fn get_state_signature(&self, height: u64) -> Option<LCV3StateSignatureRequestBody> {
1309        self.as_ref().get_state_signature(height).await
1310    }
1311}
1312
1313#[async_trait]
1314impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence> StateSignatureDataSource<N>
1315    for ApiState<N, P>
1316{
1317    async fn get_state_signature(&self, height: u64) -> Option<LCV3StateSignatureRequestBody> {
1318        self.state_signer()
1319            .await
1320            .read()
1321            .await
1322            .get_state_signature(height)
1323            .await
1324    }
1325}
1326
1327#[derive(Serialize, Deserialize, Debug, Clone)]
1328/// Representation of the RewardMerkleTreeV2 as a set of key-value pairs
1329pub struct RewardMerkleTreeV2Data {
1330    pub balances: Vec<(RewardAccountV2, RewardAmount)>,
1331}
1332
1333impl TryInto<RewardMerkleTreeV2Data> for &RewardMerkleTreeV2 {
1334    type Error = anyhow::Error;
1335    // Required method
1336    fn try_into(self) -> anyhow::Result<RewardMerkleTreeV2Data> {
1337        let num_leaves = self.num_leaves();
1338
1339        let balances: Vec<_> = self
1340            .iter()
1341            .map(|(account, balance)| (*account, *balance))
1342            .collect();
1343
1344        if balances.len() as u64 == num_leaves {
1345            Ok(RewardMerkleTreeV2Data { balances })
1346        } else {
1347            bail!(
1348                "RewardMerkleTreeV2 is incomplete, some accounts are missing. Balances length: \
1349                 {}, num_leaves: {num_leaves}.",
1350                balances.len(),
1351            );
1352        }
1353    }
1354}
1355
1356pub(crate) trait RewardMerkleTreeDataSource: Send + Sync + Clone + 'static {
1357    fn load_v1_reward_account_proof(
1358        &self,
1359        _height: u64,
1360        _account: RewardAccountV1,
1361    ) -> impl Send + Future<Output = anyhow::Result<RewardAccountQueryDataV1>>;
1362
1363    fn save_and_gc_reward_tree_v2(
1364        &self,
1365        node_state: &NodeState,
1366        height: u64,
1367        version: Version,
1368        merkle_tree: &RewardMerkleTreeV2,
1369    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1370        async move {
1371            // The merklized state loop always applies full blocks, so an incomplete
1372            // tree here indicates something is seriously wrong.
1373            let tree_data = TryInto::<RewardMerkleTreeV2Data>::try_into(merkle_tree).inspect_err(
1374                |err| tracing::error!(%err, height, "cannot persist incomplete RewardMerkleTreeV2"),
1375            )?;
1376            let serialization =
1377                bincode::serialize(&tree_data).context("Merkle tree serialization failed")?;
1378            self.persist_tree(height, serialization).await?;
1379
1380            // Skip garbage collection in tests
1381            if cfg!(any(test, feature = "testing")) {
1382                return Ok(());
1383            }
1384
1385            let finalized_hotshot_height = match node_state.finalized_hotshot_height().await {
1386                Ok(h) => h,
1387                Err(err) => {
1388                    tracing::warn!("failed to get finalized hotshot height: {err:#}");
1389                    return Ok(());
1390                },
1391            };
1392
1393            // trees at heights strictly less than the gc height are deleted
1394            //
1395            // keep recent epochs reward trees
1396            //   - staking-api-service at startup calls `reward-amounts` at
1397            //     `epoch_start - 1`, which needs the previous epoch's last-block
1398            //     tree on disk.
1399            //   - Per epoch reward (EPOCH_REWARD_VERSION+): `fetch_and_calculate`
1400            //     reads the previous epoch's last block tree to compute the next
1401            //     epoch's rewards.
1402            //
1403            // `finalized_hotshot_height`:  Reward claims
1404            //   (`reward-claim-input`) target the LightClient L1 finalization
1405            //   exactly.
1406
1407            let epoch_height = node_state
1408                .epoch_height
1409                .context("reward tree gc requires an epoch height")?;
1410            // EPOCH_REWARD_VERSION (V5)+ only persists a tree at each epoch boundary,
1411            // so 5 epochs = 5 trees on disk. Earlier versions persist a tree at
1412            // every block, so 1 epoch is already epoch_height trees; keeping more
1413            // would be expensive. We only need 1 epoch for both, but the extra
1414            // trees are cheap for V5+ so it doesn't make much of a difference.
1415            let epochs_to_retain = if version >= versions::EPOCH_REWARD_VERSION {
1416                5
1417            } else {
1418                1
1419            };
1420            let current_epoch = epoch_from_block_number(height, epoch_height);
1421            // First block of the oldest epoch we still want to retain.
1422            let epoch_start_block = current_epoch.saturating_sub(epochs_to_retain) * epoch_height;
1423
1424            let gc_height = epoch_start_block.min(finalized_hotshot_height);
1425
1426            if let Err(err) = self.garbage_collect(gc_height).await {
1427                tracing::info!(gc_height, "failed to garbage collect: {err:#}");
1428            }
1429
1430            Ok(())
1431        }
1432    }
1433
1434    fn persist_reward_proofs(
1435        &self,
1436        node_state: &NodeState,
1437        height: u64,
1438        version: Version,
1439    ) -> impl Send + Future<Output = anyhow::Result<()>>;
1440
1441    fn load_reward_merkle_tree_v2(
1442        &self,
1443        height: u64,
1444    ) -> impl Send + Future<Output = anyhow::Result<PermittedRewardMerkleTreeV2>> {
1445        async move {
1446            let tree_bytes = self.load_tree(height).await?;
1447
1448            let tree_data = bincode::deserialize::<RewardMerkleTreeV2Data>(&tree_bytes).context(
1449                "Failed to deserialize RewardMerkleTreeV2 for height {height} from storage; this \
1450                 should never happen.",
1451            )?;
1452
1453            PermittedRewardMerkleTreeV2::try_from_kv_set(tree_data.balances)
1454                .await
1455                .context("Failed to reconstruct reward merkle tree from storage")
1456        }
1457    }
1458
1459    /// Returns the RewardMerkleTreeV2 for height <= requested height
1460    ///
1461    /// After V5 the tree is only written at epoch boundaries, so `reward_merkle_tree_v2_data`
1462    /// has no row for most heights. Within an epoch the tree doesn't change, so the previous
1463    /// boundary's tree matches the current block's reward root but only if we're actually in
1464    /// the same epoch. The caller is responsible for checking the returned tree's commitment
1465    /// against the header at `height`.
1466    /// if they differ we loaded a tree from an older epoch.
1467    fn load_latest_reward_merkle_tree_v2(
1468        &self,
1469        height: u64,
1470    ) -> impl Send + Future<Output = anyhow::Result<PermittedRewardMerkleTreeV2>> {
1471        async move {
1472            let tree_bytes = self.load_latest_tree(height).await?;
1473
1474            let tree_data = bincode::deserialize::<RewardMerkleTreeV2Data>(&tree_bytes)
1475                .context("Failed to deserialize RewardMerkleTreeV2 from storage")?;
1476
1477            PermittedRewardMerkleTreeV2::try_from_kv_set(tree_data.balances)
1478                .await
1479                .context("Failed to reconstruct reward merkle tree from storage")
1480        }
1481    }
1482
1483    fn load_reward_account_proof_v2(
1484        &self,
1485        _height: u64,
1486        _account: RewardAccountV2,
1487    ) -> impl Send + Future<Output = anyhow::Result<RewardAccountQueryDataV2>> {
1488        async {
1489            bail!("load_reward_account_proof_v2 is not supported for this data source");
1490        }
1491    }
1492
1493    fn load_latest_reward_account_proof_v2(
1494        &self,
1495        account: RewardAccountV2,
1496    ) -> impl Send + Future<Output = anyhow::Result<RewardAccountQueryDataV2>> {
1497        async move {
1498            let serialized_account = bincode::serialize(&account).context(
1499                "Failed to serialize RewardAccountV2 for lookup; this should never happen.",
1500            )?;
1501            let proof_bytes = self.load_latest_proof(serialized_account).await?;
1502
1503            bincode::deserialize::<RewardAccountQueryDataV2>(&proof_bytes).context(
1504                "Failed to deserialize RewardAccountQueryDataV2 for account {account} from \
1505                 storage; this should never happen.",
1506            )
1507        }
1508    }
1509
1510    fn persist_tree(
1511        &self,
1512        height: u64,
1513        merkle_tree: Vec<u8>,
1514    ) -> impl Send + Future<Output = anyhow::Result<()>>;
1515
1516    fn load_tree(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>>;
1517
1518    /// Load the latest serialized reward merkle tree v2 at height `<= height`.
1519    fn load_latest_tree(&self, height: u64)
1520    -> impl Send + Future<Output = anyhow::Result<Vec<u8>>>;
1521
1522    fn persist_proofs(
1523        &self,
1524        height: u64,
1525        proofs: impl Iterator<Item = (Vec<u8>, Vec<u8>)> + Send,
1526    ) -> impl Send + Future<Output = anyhow::Result<()>>;
1527
1528    fn load_proof(
1529        &self,
1530        height: u64,
1531        account: Vec<u8>,
1532        epoch_height: u64,
1533    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>>;
1534
1535    fn load_latest_proof(
1536        &self,
1537        account: Vec<u8>,
1538    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>>;
1539
1540    fn proof_exists(&self, height: u64) -> impl Send + Future<Output = bool>;
1541
1542    /// garbage collects merkle tree data for blocks strictly older than `height`
1543    fn garbage_collect(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<()>>;
1544}
1545
1546impl RewardMerkleTreeDataSource for hotshot_query_service::data_source::MetricsDataSource {
1547    fn load_v1_reward_account_proof(
1548        &self,
1549        _height: u64,
1550        _account: RewardAccountV1,
1551    ) -> impl Send + Future<Output = anyhow::Result<RewardAccountQueryDataV1>> {
1552        async {
1553            bail!("reward merklized state is not supported for this data source");
1554        }
1555    }
1556
1557    fn persist_reward_proofs(
1558        &self,
1559        _node_state: &NodeState,
1560        _height: u64,
1561        _version: Version,
1562    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1563        async {
1564            bail!("reward merklized state is not supported for this data source");
1565        }
1566    }
1567
1568    fn persist_tree(
1569        &self,
1570        _height: u64,
1571        _merkle_tree: Vec<u8>,
1572    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1573        async move {
1574            bail!("reward merklized state is not supported for this data source");
1575        }
1576    }
1577
1578    fn load_tree(&self, _height: u64) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1579        async move {
1580            bail!("reward merklized state is not supported for this data source");
1581        }
1582    }
1583
1584    fn load_latest_tree(
1585        &self,
1586        _height: u64,
1587    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1588        async move {
1589            bail!("reward merklized state is not supported for this data source");
1590        }
1591    }
1592
1593    fn garbage_collect(&self, _height: u64) -> impl Send + Future<Output = anyhow::Result<()>> {
1594        async move {
1595            bail!("reward merklized state is not supported for this data source");
1596        }
1597    }
1598
1599    fn persist_proofs(
1600        &self,
1601        _height: u64,
1602        _proofs: impl Iterator<Item = (Vec<u8>, Vec<u8>)> + Send,
1603    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1604        async move {
1605            bail!("reward merklized state is not supported for this data source");
1606        }
1607    }
1608
1609    fn load_proof(
1610        &self,
1611        _height: u64,
1612        _account: Vec<u8>,
1613        _epoch_height: u64,
1614    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1615        async move {
1616            bail!("reward merklized state is not supported for this data source");
1617        }
1618    }
1619
1620    fn load_latest_proof(
1621        &self,
1622        _account: Vec<u8>,
1623    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1624        async move {
1625            bail!("reward merklized state is not supported for this data source");
1626        }
1627    }
1628
1629    fn proof_exists(&self, _height: u64) -> impl Send + Future<Output = bool> {
1630        async move { false }
1631    }
1632}
1633
1634impl<T, S> RewardMerkleTreeDataSource
1635    for hotshot_query_service::data_source::ExtensibleDataSource<T, S>
1636where
1637    T: RewardMerkleTreeDataSource,
1638    S: Send + Sync + Clone + NodeStateDataSource + 'static,
1639{
1640    async fn load_v1_reward_account_proof(
1641        &self,
1642        height: u64,
1643        account: RewardAccountV1,
1644    ) -> anyhow::Result<RewardAccountQueryDataV1> {
1645        self.inner()
1646            .load_v1_reward_account_proof(height, account)
1647            .await
1648    }
1649
1650    async fn load_reward_account_proof_v2(
1651        &self,
1652        height: u64,
1653        account: RewardAccountV2,
1654    ) -> anyhow::Result<RewardAccountQueryDataV2> {
1655        let epoch_height = self
1656            .as_ref()
1657            .node_state()
1658            .await
1659            .epoch_height
1660            .context("epoch height not found")?;
1661        let serialized_account = bincode::serialize(&account)
1662            .context("Failed to serialize RewardAccountV2 for lookup; this should never happen.")?;
1663        let proof_bytes = self
1664            .inner()
1665            .load_proof(height, serialized_account, epoch_height)
1666            .await?;
1667
1668        bincode::deserialize::<RewardAccountQueryDataV2>(&proof_bytes)
1669            .context("Failed to deserialize RewardAccountQueryDataV2 from storage")
1670    }
1671
1672    fn persist_tree(
1673        &self,
1674        height: u64,
1675        merkle_tree: Vec<u8>,
1676    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1677        async move { self.inner().persist_tree(height, merkle_tree).await }
1678    }
1679
1680    fn load_tree(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1681        async move { self.inner().load_tree(height).await }
1682    }
1683
1684    fn load_latest_tree(
1685        &self,
1686        height: u64,
1687    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1688        async move { self.inner().load_latest_tree(height).await }
1689    }
1690
1691    fn garbage_collect(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<()>> {
1692        async move { self.inner().garbage_collect(height).await }
1693    }
1694
1695    fn persist_proofs(
1696        &self,
1697        height: u64,
1698        proofs: impl Iterator<Item = (Vec<u8>, Vec<u8>)> + Send,
1699    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1700        async move { self.inner().persist_proofs(height, proofs).await }
1701    }
1702
1703    fn load_proof(
1704        &self,
1705        height: u64,
1706        account: Vec<u8>,
1707        epoch_height: u64,
1708    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1709        async move { self.inner().load_proof(height, account, epoch_height).await }
1710    }
1711
1712    fn load_latest_proof(
1713        &self,
1714        account: Vec<u8>,
1715    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1716        async move { self.inner().load_latest_proof(account).await }
1717    }
1718
1719    fn proof_exists(&self, height: u64) -> impl Send + Future<Output = bool> {
1720        async move { self.inner().proof_exists(height).await }
1721    }
1722
1723    fn persist_reward_proofs(
1724        &self,
1725        node_state: &NodeState,
1726        height: u64,
1727        version: Version,
1728    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1729        async move {
1730            self.inner()
1731                .persist_reward_proofs(node_state, height, version)
1732                .await
1733        }
1734    }
1735}
1736
1737// Implement Reward MerkleTreeDataSource for Arc<D> to allow shared ownership
1738impl<D> RewardMerkleTreeDataSource for Arc<D>
1739where
1740    D: RewardMerkleTreeDataSource,
1741{
1742    async fn load_v1_reward_account_proof(
1743        &self,
1744        height: u64,
1745        account: RewardAccountV1,
1746    ) -> anyhow::Result<RewardAccountQueryDataV1> {
1747        (**self).load_v1_reward_account_proof(height, account).await
1748    }
1749
1750    fn persist_tree(
1751        &self,
1752        height: u64,
1753        merkle_tree: Vec<u8>,
1754    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1755        async move { (**self).persist_tree(height, merkle_tree).await }
1756    }
1757
1758    fn load_tree(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1759        async move { (**self).load_tree(height).await }
1760    }
1761
1762    fn load_latest_tree(
1763        &self,
1764        height: u64,
1765    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1766        async move { (**self).load_latest_tree(height).await }
1767    }
1768
1769    fn load_reward_merkle_tree_v2(
1770        &self,
1771        height: u64,
1772    ) -> impl Send + Future<Output = anyhow::Result<PermittedRewardMerkleTreeV2>> {
1773        async move { (**self).load_reward_merkle_tree_v2(height).await }
1774    }
1775
1776    fn load_reward_account_proof_v2(
1777        &self,
1778        height: u64,
1779        account: RewardAccountV2,
1780    ) -> impl Send + Future<Output = anyhow::Result<RewardAccountQueryDataV2>> {
1781        async move { (**self).load_reward_account_proof_v2(height, account).await }
1782    }
1783
1784    fn persist_proofs(
1785        &self,
1786        height: u64,
1787        proofs: impl Iterator<Item = (Vec<u8>, Vec<u8>)> + Send,
1788    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1789        async move { (**self).persist_proofs(height, proofs).await }
1790    }
1791
1792    fn persist_reward_proofs(
1793        &self,
1794        node_state: &NodeState,
1795        height: u64,
1796        version: Version,
1797    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1798        async move {
1799            (**self)
1800                .persist_reward_proofs(node_state, height, version)
1801                .await
1802        }
1803    }
1804
1805    fn load_proof(
1806        &self,
1807        height: u64,
1808        account: Vec<u8>,
1809        epoch_height: u64,
1810    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1811        async move { (**self).load_proof(height, account, epoch_height).await }
1812    }
1813
1814    fn proof_exists(&self, height: u64) -> impl Send + Future<Output = bool> {
1815        async move { (**self).proof_exists(height).await }
1816    }
1817
1818    fn load_latest_proof(
1819        &self,
1820        account: Vec<u8>,
1821    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1822        async move { (**self).load_latest_proof(account).await }
1823    }
1824
1825    fn garbage_collect(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<()>> {
1826        async move { (**self).garbage_collect(height).await }
1827    }
1828}
1829
1830/// [`Provider`] implementation wrapping a lazy [`LightClient`].
1831///
1832/// The [`LightClient`] requires a genesis to initialize itself, which we can get from the
1833/// [`ApiState`]. However, the [`Provider`] instance must be provided to the API data source at
1834/// initialization time, while the [`ApiState`] is only initialized lazily. This is a provider
1835/// implementation which is itself initialized lazily: [`Provider::fetch`] calls will time out until
1836/// the underlying [`ApiState`] is fully initialized, at which point this provider will start
1837/// serving fetches using the [`LightClient`].
1838#[derive(Debug)]
1839struct LightClientProvider {
1840    light_client: BoxLazy<LightClient<SqliteStorage, FallbackClient<QueryServiceClient>>>,
1841}
1842
1843impl LightClientProvider {
1844    pub async fn new<N, P>(
1845        peers: impl IntoIterator<Item = Url>,
1846        state: ApiState<N, P>,
1847        opt: LightClientOptions,
1848        db_opt: LightClientSqliteOptions,
1849    ) -> anyhow::Result<Self>
1850    where
1851        N: ConnectedNetwork<PubKey>,
1852        P: SequencerPersistence,
1853    {
1854        let db = db_opt
1855            .connect()
1856            .await
1857            .context("creating SQLite database for light client")?;
1858        let client = FallbackClient::new(peers.into_iter().map(QueryServiceClient::new).collect())?;
1859        let init_light_client = async move {
1860            let config = state.network_config().await;
1861            let chain_id = state.node_state().await.genesis_chain_config.chain_id;
1862            let epoch_height = config.config.epoch_height;
1863            let first_epoch =
1864                epoch_from_block_number(config.config.epoch_start_block, epoch_height);
1865
1866            let genesis = Genesis {
1867                epoch_height,
1868
1869                // Dynamic state starts from the third epoch, since we need the prior epoch's root
1870                // to have the upgraded header with the stake table hash.
1871                first_epoch_with_dynamic_stake_table: EpochNumber::new(first_epoch + 2),
1872
1873                stake_table: config
1874                    .config
1875                    .known_nodes_with_stake
1876                    .into_iter()
1877                    .map(|peer| peer.stake_table_entry)
1878                    .collect(),
1879
1880                chain_id,
1881            };
1882            LightClient::from_genesis_with_options(db, client, genesis, opt)
1883        };
1884        Ok(Self {
1885            light_client: Arc::pin(Lazy::from_future(init_light_client.boxed())),
1886        })
1887    }
1888}
1889
1890#[async_trait]
1891impl<T> Provider<SeqTypes, T> for LightClientProvider
1892where
1893    T: fetching::Request<SeqTypes> + 'static,
1894    LightClient<SqliteStorage, FallbackClient<QueryServiceClient>>: Provider<SeqTypes, T>,
1895{
1896    async fn fetch(&self, req: T) -> Option<T::Response> {
1897        self.light_client.as_ref().get().await.fetch(req).await
1898    }
1899}
1900
1901#[cfg(any(test, feature = "testing"))]
1902pub mod test_helpers {
1903    use std::{
1904        cmp::max,
1905        collections::HashSet,
1906        sync::atomic::{AtomicU32, Ordering},
1907        time::Duration,
1908    };
1909
1910    use alloy::{
1911        network::EthereumWallet,
1912        primitives::{Address, U256, utils::parse_ether},
1913        providers::{Provider, ProviderBuilder, ext::AnvilApi},
1914        signers::local::PrivateKeySigner,
1915    };
1916    use committable::{Commitment, Committable};
1917    use espresso_contract_deployer::{
1918        Contract, Contracts, DEFAULT_EXIT_ESCROW_PERIOD_SECONDS, builder::DeployerArgsBuilder,
1919        network_config::light_client_genesis_from_stake_table,
1920    };
1921    use espresso_types::{
1922        MOCK_SEQUENCER_VERSIONS, NamespaceId, ValidatedState,
1923        v0::traits::{NullEventConsumer, PersistenceOptions, SequencerPersistence, StateCatchup},
1924    };
1925    use futures::{
1926        future::{FutureExt, join_all},
1927        stream::{Stream, StreamExt},
1928    };
1929    use hotshot::types::{Event, EventType};
1930    use hotshot_contract_adapter::stake_table::StakeTableContractVersion;
1931    use hotshot_types::{
1932        event::LeafInfo, light_client::LCV3StateSignatureRequestBody,
1933        new_protocol::CoordinatorEvent, traits::metrics::NoMetrics,
1934    };
1935    use http_client::{Client, error::ClientErr};
1936    use itertools::izip;
1937    use jf_merkle_tree_compat::{MerkleCommitment, MerkleTreeScheme};
1938    use staking_cli::{
1939        Transaction as StakingTransaction,
1940        demo::{DelegationConfig, StakingTransactions},
1941    };
1942    use tempfile::TempDir;
1943    use test_utils::reserve_tcp_port;
1944    use tokio::time::sleep;
1945    use vbs::version::StaticVersion;
1946    use versions::{EPOCH_VERSION, Upgrade};
1947
1948    use super::*;
1949    use crate::{
1950        catchup::NullStateCatchup,
1951        network,
1952        persistence::no_storage,
1953        testing::{
1954            TestConfig, TestConfigBuilder, deploy_stake_table, run_legacy_builder,
1955            wait_for_decide_on_handle, wait_for_epochs,
1956        },
1957    };
1958
1959    pub const STAKE_TABLE_CAPACITY_FOR_TEST: usize = 10;
1960
1961    pub struct TestNetwork<P: PersistenceOptions, const NUM_NODES: usize> {
1962        pub server: SequencerContext<network::Memory, P::Persistence>,
1963        pub peers: Vec<SequencerContext<network::Memory, P::Persistence>>,
1964        pub cfg: TestConfig<{ NUM_NODES }>,
1965        // todo (abdul): remove this when fs storage is removed
1966        pub temp_dir: Option<TempDir>,
1967        pub contracts: Option<Contracts>,
1968        /// Deferred node indices not yet started (see [`Self::start_deferred_node`]).
1969        deferred: Vec<usize>,
1970    }
1971
1972    pub struct TestNetworkConfig<const NUM_NODES: usize, P, C>
1973    where
1974        P: PersistenceOptions,
1975        C: StateCatchup + 'static,
1976    {
1977        state: [ValidatedState; NUM_NODES],
1978        persistence: [P; NUM_NODES],
1979        catchup: [C; NUM_NODES],
1980        network_config: TestConfig<{ NUM_NODES }>,
1981        api_config: Options,
1982        contracts: Option<Contracts>,
1983        deferred_start: Vec<usize>,
1984    }
1985
1986    impl<const NUM_NODES: usize, P, C> TestNetworkConfig<{ NUM_NODES }, P, C>
1987    where
1988        P: PersistenceOptions,
1989        C: StateCatchup + 'static,
1990    {
1991        pub fn states(&self) -> [ValidatedState; NUM_NODES] {
1992            self.state.clone()
1993        }
1994    }
1995
1996    #[derive(Clone)]
1997    pub struct TestNetworkConfigBuilder<const NUM_NODES: usize, P, C>
1998    where
1999        P: PersistenceOptions,
2000        C: StateCatchup + 'static,
2001    {
2002        state: [ValidatedState; NUM_NODES],
2003        persistence: Option<[P; NUM_NODES]>,
2004        catchup: Option<[C; NUM_NODES]>,
2005        api_config: Option<Options>,
2006        network_config: Option<TestConfig<{ NUM_NODES }>>,
2007        contracts: Option<Contracts>,
2008        initial_token_supply: Option<U256>,
2009        deferred_start: Vec<usize>,
2010    }
2011
2012    impl Default for TestNetworkConfigBuilder<5, no_storage::Options, NullStateCatchup> {
2013        fn default() -> Self {
2014            TestNetworkConfigBuilder {
2015                state: std::array::from_fn(|_| ValidatedState::default()),
2016                persistence: Some([no_storage::Options; 5]),
2017                catchup: Some(std::array::from_fn(|_| NullStateCatchup::default())),
2018                network_config: None,
2019                api_config: None,
2020                contracts: None,
2021                initial_token_supply: None,
2022                deferred_start: Vec::new(),
2023            }
2024        }
2025    }
2026
2027    impl<const NUM_NODES: usize>
2028        TestNetworkConfigBuilder<{ NUM_NODES }, no_storage::Options, NullStateCatchup>
2029    {
2030        pub fn with_num_nodes()
2031        -> TestNetworkConfigBuilder<{ NUM_NODES }, no_storage::Options, NullStateCatchup> {
2032            TestNetworkConfigBuilder {
2033                state: std::array::from_fn(|_| ValidatedState::default()),
2034                persistence: Some([no_storage::Options; { NUM_NODES }]),
2035                catchup: Some(std::array::from_fn(|_| NullStateCatchup::default())),
2036                network_config: None,
2037                api_config: None,
2038                contracts: None,
2039                initial_token_supply: None,
2040                deferred_start: Vec::new(),
2041            }
2042        }
2043    }
2044
2045    impl<const NUM_NODES: usize, P, C> TestNetworkConfigBuilder<{ NUM_NODES }, P, C>
2046    where
2047        P: PersistenceOptions,
2048        C: StateCatchup + 'static,
2049    {
2050        pub fn states(mut self, state: [ValidatedState; NUM_NODES]) -> Self {
2051            self.state = state;
2052            self
2053        }
2054
2055        pub fn initial_token_supply(mut self, supply: U256) -> Self {
2056            self.initial_token_supply = Some(supply);
2057            self
2058        }
2059
2060        pub fn persistences<NP: PersistenceOptions>(
2061            self,
2062            persistence: [NP; NUM_NODES],
2063        ) -> TestNetworkConfigBuilder<{ NUM_NODES }, NP, C> {
2064            TestNetworkConfigBuilder {
2065                state: self.state,
2066                catchup: self.catchup,
2067                network_config: self.network_config,
2068                api_config: self.api_config,
2069                persistence: Some(persistence),
2070                contracts: self.contracts,
2071                initial_token_supply: self.initial_token_supply,
2072                deferred_start: self.deferred_start,
2073            }
2074        }
2075
2076        pub fn api_config(mut self, api_config: Options) -> Self {
2077            self.api_config = Some(api_config);
2078            self
2079        }
2080
2081        pub fn catchups<NC: StateCatchup + 'static>(
2082            self,
2083            catchup: [NC; NUM_NODES],
2084        ) -> TestNetworkConfigBuilder<{ NUM_NODES }, P, NC> {
2085            TestNetworkConfigBuilder {
2086                state: self.state,
2087                catchup: Some(catchup),
2088                network_config: self.network_config,
2089                api_config: self.api_config,
2090                persistence: self.persistence,
2091                contracts: self.contracts,
2092                initial_token_supply: self.initial_token_supply,
2093                deferred_start: self.deferred_start,
2094            }
2095        }
2096
2097        /// Defers starting the nodes at the given (trailing) indices; they
2098        /// join later via [`TestNetwork::start_deferred_node`].
2099        pub fn deferred_start(mut self, indices: &[usize]) -> Self {
2100            self.deferred_start = indices.to_vec();
2101            self
2102        }
2103
2104        pub fn network_config(mut self, network_config: TestConfig<{ NUM_NODES }>) -> Self {
2105            self.network_config = Some(network_config);
2106            self
2107        }
2108
2109        pub fn contracts(mut self, contracts: Contracts) -> Self {
2110            self.contracts = Some(contracts);
2111            self
2112        }
2113
2114        /// Setup for POS testing. Deploys contracts and adds the
2115        /// stake table address to state. Must be called before `build()`.
2116        pub async fn pos_hook(
2117            self,
2118            delegation_config: DelegationConfig,
2119            stake_table_version: StakeTableContractVersion,
2120            upgrade: Upgrade,
2121        ) -> anyhow::Result<Self> {
2122            let registered: Vec<usize> = (0..NUM_NODES).collect();
2123            self.pos_hook_with_registered(
2124                delegation_config,
2125                stake_table_version,
2126                upgrade,
2127                &registered,
2128            )
2129            .await
2130        }
2131
2132        /// Like [`Self::pos_hook`], but registers only the validators at the
2133        /// `registered` node indices on the stake table contract. The other
2134        /// nodes still run from genesis (which seeds the first two epochs)
2135        /// and can be registered mid-test via [`register_validators`].
2136        pub async fn pos_hook_with_registered(
2137            self,
2138            delegation_config: DelegationConfig,
2139            stake_table_version: StakeTableContractVersion,
2140            upgrade: Upgrade,
2141            registered: &[usize],
2142        ) -> anyhow::Result<Self> {
2143            if upgrade.base < EPOCH_VERSION && upgrade.target < EPOCH_VERSION {
2144                panic!("given version does not require pos deployment");
2145            };
2146
2147            let network_config = self
2148                .network_config
2149                .as_ref()
2150                .expect("network_config is required");
2151
2152            let l1_url = network_config.l1_url();
2153            let signer = network_config.signer();
2154            let deployer = ProviderBuilder::new()
2155                .wallet(EthereumWallet::from(signer.clone()))
2156                .connect_http(l1_url.clone());
2157
2158            let blocks_per_epoch = network_config.hotshot_config().epoch_height;
2159            let epoch_start_block = network_config.hotshot_config().epoch_start_block;
2160            let (genesis_state, genesis_stake) = light_client_genesis_from_stake_table(
2161                &network_config.hotshot_config().hotshot_stake_table(),
2162                STAKE_TABLE_CAPACITY_FOR_TEST,
2163            )
2164            .unwrap();
2165
2166            let mut contracts = Contracts::new();
2167            let args = DeployerArgsBuilder::default()
2168                .deployer(deployer.clone())
2169                .rpc_url(l1_url.clone())
2170                .mock_light_client(true)
2171                .genesis_lc_state(genesis_state)
2172                .genesis_st_state(genesis_stake)
2173                .blocks_per_epoch(blocks_per_epoch)
2174                .epoch_start_block(epoch_start_block)
2175                .exit_escrow_period(U256::from(max(
2176                    blocks_per_epoch * 15 + 100,
2177                    DEFAULT_EXIT_ESCROW_PERIOD_SECONDS,
2178                )))
2179                .multisig_pauser(signer.address())
2180                .token_name("Espresso".to_string())
2181                .token_symbol("ESP".to_string())
2182                .initial_token_supply(self.initial_token_supply.unwrap_or(U256::from(100000u64)))
2183                .ops_timelock_delay(U256::from(0))
2184                .ops_timelock_admin(signer.address())
2185                .ops_timelock_proposers(vec![signer.address()])
2186                .ops_timelock_executors(vec![signer.address()])
2187                .safe_exit_timelock_delay(U256::from(10))
2188                .safe_exit_timelock_admin(signer.address())
2189                .safe_exit_timelock_proposers(vec![signer.address()])
2190                .safe_exit_timelock_executors(vec![signer.address()])
2191                .build()
2192                .unwrap();
2193
2194            deploy_stake_table(&args, stake_table_version, &mut contracts)
2195                .await
2196                .context("failed to deploy contracts")?;
2197
2198            let stake_table_address = contracts
2199                .address(Contract::StakeTableProxy)
2200                .expect("StakeTableProxy address not found");
2201
2202            StakingTransactions::create(
2203                l1_url.clone(),
2204                &deployer,
2205                stake_table_address,
2206                network_config.staking_key_sets(registered),
2207                None,
2208                delegation_config,
2209            )
2210            .await
2211            .expect("stake table setup failed")
2212            .apply_all()
2213            .await
2214            .expect("send all txns failed");
2215
2216            // enable interval mining with a 1s interval.
2217            // This ensures that blocks are finalized every second, even when there are no transactions.
2218            // It's useful for testing stake table updates,
2219            // which rely on the finalized L1 block number.
2220            if let Some(anvil) = network_config.anvil() {
2221                anvil
2222                    .anvil_set_interval_mining(1)
2223                    .await
2224                    .expect("interval mining");
2225            }
2226
2227            // Add stake table address to `ChainConfig` (held in state),
2228            // avoiding overwrite other values. Base fee is set to `0` to avoid
2229            // unnecessary catchup of `FeeState`.
2230            let state = self.state[0].clone();
2231            let chain_config = if let Some(cf) = state.chain_config.resolve() {
2232                ChainConfig {
2233                    base_fee: 0.into(),
2234                    stake_table_contract: Some(stake_table_address),
2235                    ..cf
2236                }
2237            } else {
2238                ChainConfig {
2239                    base_fee: 0.into(),
2240                    stake_table_contract: Some(stake_table_address),
2241                    ..Default::default()
2242                }
2243            };
2244
2245            let state = ValidatedState {
2246                chain_config: chain_config.into(),
2247                ..state
2248            };
2249            Ok(self
2250                .states(std::array::from_fn(|_| state.clone()))
2251                .contracts(contracts))
2252        }
2253
2254        pub fn build(self) -> TestNetworkConfig<{ NUM_NODES }, P, C> {
2255            TestNetworkConfig {
2256                state: self.state,
2257                persistence: self.persistence.unwrap(),
2258                catchup: self.catchup.unwrap(),
2259                network_config: self.network_config.unwrap(),
2260                api_config: self.api_config.unwrap(),
2261                contracts: self.contracts,
2262                deferred_start: self.deferred_start,
2263            }
2264        }
2265    }
2266
2267    impl<P: PersistenceOptions, const NUM_NODES: usize> TestNetwork<P, { NUM_NODES }> {
2268        pub async fn new<C: StateCatchup + 'static>(
2269            cfg: TestNetworkConfig<{ NUM_NODES }, P, C>,
2270            upgrade: versions::Upgrade,
2271        ) -> Self {
2272            let mut cfg = cfg;
2273            let mut builder_tasks = Vec::new();
2274
2275            let chain_config = cfg.state[0].chain_config.resolve();
2276            if chain_config.is_none() {
2277                tracing::warn!("Chain config is not set, using default max_block_size");
2278            }
2279            let (task, builder_url) = run_legacy_builder::<{ NUM_NODES }>(
2280                cfg.network_config.builder_port(),
2281                chain_config.map(|c| *c.max_block_size),
2282            )
2283            .await;
2284            builder_tasks.push(task);
2285            cfg.network_config
2286                .set_builder_urls(vec1::vec1![builder_url.clone()]);
2287
2288            // add default storage if none is provided as query module is now required
2289            let mut opt = cfg.api_config.clone();
2290            let temp_dir = if opt.storage_fs.is_none() && opt.storage_sql.is_none() {
2291                let temp_dir = tempfile::tempdir().unwrap();
2292                opt = opt.query_fs(
2293                    Default::default(),
2294                    crate::persistence::fs::Options::new(temp_dir.path().to_path_buf()),
2295                );
2296                Some(temp_dir)
2297            } else {
2298                None
2299            };
2300
2301            let deferred = cfg.deferred_start.clone();
2302            assert!(
2303                deferred.len() < NUM_NODES,
2304                "node 0 runs the API server and cannot be deferred"
2305            );
2306            assert_eq!(
2307                deferred,
2308                (NUM_NODES - deferred.len()..NUM_NODES).collect::<Vec<_>>(),
2309                "deferred_start must be the trailing indices so `node(i)` stays aligned"
2310            );
2311
2312            let mut nodes = join_all(
2313                izip!(cfg.state, cfg.persistence, cfg.catchup)
2314                    .enumerate()
2315                    .filter(|(i, _)| !deferred.contains(i))
2316                    .map(|(i, (state, persistence, state_peers))| {
2317                        let opt = opt.clone();
2318                        let cfg = &cfg.network_config;
2319                        let upgrades_map = cfg.upgrades();
2320                        async move {
2321                            if i == 0 {
2322                                opt.serve(|metrics, consumer, storage| {
2323                                    let cfg = cfg.clone();
2324                                    async move {
2325                                        Ok(cfg
2326                                            .init_node(
2327                                                0,
2328                                                state,
2329                                                persistence,
2330                                                Some(state_peers),
2331                                                storage,
2332                                                &*metrics,
2333                                                STAKE_TABLE_CAPACITY_FOR_TEST,
2334                                                consumer,
2335                                                upgrade,
2336                                                upgrades_map,
2337                                            )
2338                                            .await)
2339                                    }
2340                                    .boxed()
2341                                })
2342                                .await
2343                                .unwrap()
2344                            } else {
2345                                cfg.init_node(
2346                                    i,
2347                                    state,
2348                                    persistence,
2349                                    Some(state_peers),
2350                                    None,
2351                                    &NoMetrics,
2352                                    STAKE_TABLE_CAPACITY_FOR_TEST,
2353                                    NullEventConsumer,
2354                                    upgrade,
2355                                    upgrades_map,
2356                                )
2357                                .await
2358                            }
2359                        }
2360                        .boxed()
2361                    }),
2362            )
2363            .await;
2364
2365            let handle_0 = &nodes[0];
2366
2367            // Hook the builder(s) up to the event stream from the first node
2368            for builder_task in builder_tasks {
2369                builder_task.start(Box::new(
2370                    handle_0
2371                        .consensus_handle()
2372                        .legacy_consensus()
2373                        .read()
2374                        .await
2375                        .event_stream(),
2376                ));
2377            }
2378
2379            for ctx in &nodes {
2380                ctx.start_consensus().await;
2381            }
2382
2383            let server = nodes.remove(0);
2384            let peers = nodes;
2385
2386            Self {
2387                server,
2388                peers,
2389                cfg: cfg.network_config,
2390                temp_dir,
2391                contracts: cfg.contracts,
2392                deferred,
2393            }
2394        }
2395
2396        /// Initializes and starts a node deferred at construction (see
2397        /// [`TestNetworkConfigBuilder::deferred_start`]), in ascending index
2398        /// order; the node is then reachable via [`Self::node`] as usual.
2399        pub async fn start_deferred_node<C: StateCatchup + 'static>(
2400            &mut self,
2401            i: usize,
2402            state: ValidatedState,
2403            persistence: P,
2404            catchup: C,
2405            upgrade: versions::Upgrade,
2406        ) -> &SequencerContext<network::Memory, P::Persistence> {
2407            assert_eq!(
2408                self.deferred.first(),
2409                Some(&i),
2410                "deferred nodes must be started in ascending index order"
2411            );
2412            self.deferred.remove(0);
2413
2414            let ctx = self
2415                .init_and_start(i, state, persistence, catchup, upgrade)
2416                .await;
2417            self.peers.push(ctx);
2418            self.peers.last().unwrap()
2419        }
2420
2421        /// Shuts the node at index `i` down and reinitializes it from the
2422        /// network's current configuration, picking up any rotated consensus
2423        /// keys or coordinator address (see [`TestConfig::set_consensus_keys`]
2424        /// and [`TestConfig::set_coordinator_addr`]). Node 0 hosts the query
2425        /// API and cannot be restarted this way.
2426        pub async fn restart_node<C: StateCatchup + 'static>(
2427            &mut self,
2428            i: usize,
2429            state: ValidatedState,
2430            persistence: P,
2431            catchup: C,
2432            upgrade: versions::Upgrade,
2433        ) -> &SequencerContext<network::Memory, P::Persistence> {
2434            assert_ne!(i, 0, "node 0 runs the API server and cannot be restarted");
2435            assert!(
2436                !self.deferred.contains(&i),
2437                "node {i} was deferred and has not been started yet"
2438            );
2439            self.peers[i - 1].shut_down().await;
2440            // The restarted node may rebind the very coordinator port it
2441            // just released, but `shut_down` cannot await the listener drop:
2442            // aborted tasks holding network senders keep it alive. Poll
2443            // until the address is actually bindable again.
2444            let addr = self.cfg.coordinator_addr(i).to_string();
2445            timeout(Duration::from_secs(60), async {
2446                while std::net::TcpListener::bind(&addr).is_err() {
2447                    sleep(Duration::from_millis(100)).await;
2448                }
2449            })
2450            .await
2451            .expect("shut-down node did not release its coordinator port");
2452
2453            let ctx = self
2454                .init_and_start(i, state, persistence, catchup, upgrade)
2455                .await;
2456            self.peers[i - 1] = ctx;
2457            &self.peers[i - 1]
2458        }
2459
2460        /// Initializes node `i` from the network's current configuration and
2461        /// starts consensus on it, the same way construction does.
2462        async fn init_and_start<C: StateCatchup + 'static>(
2463            &self,
2464            i: usize,
2465            state: ValidatedState,
2466            persistence: P,
2467            catchup: C,
2468            upgrade: versions::Upgrade,
2469        ) -> SequencerContext<network::Memory, P::Persistence> {
2470            let ctx = self
2471                .cfg
2472                .init_node(
2473                    i,
2474                    state,
2475                    persistence,
2476                    Some(catchup),
2477                    None,
2478                    &NoMetrics,
2479                    STAKE_TABLE_CAPACITY_FOR_TEST,
2480                    NullEventConsumer,
2481                    upgrade,
2482                    self.cfg.upgrades(),
2483                )
2484                .await;
2485            ctx.start_consensus().await;
2486            ctx
2487        }
2488
2489        pub async fn stop_consensus(&mut self) {
2490            self.server.shutdown_consensus().await;
2491
2492            for ctx in &mut self.peers {
2493                ctx.shutdown_consensus().await;
2494            }
2495        }
2496
2497        /// The context of the node at index `i` (node 0 is the API server).
2498        pub fn node(&self, i: usize) -> &SequencerContext<network::Memory, P::Persistence> {
2499            if i == 0 {
2500                &self.server
2501            } else {
2502                &self.peers[i - 1]
2503            }
2504        }
2505    }
2506
2507    /// Registers and delegates to a batch of new validators mid-run, funding
2508    /// their L1 accounts from the network's deployer signer.
2509    pub async fn register_validators<const NUM_NODES: usize>(
2510        cfg: &TestConfig<NUM_NODES>,
2511        stake_table: Address,
2512        indices: &[usize],
2513        delegation_config: DelegationConfig,
2514    ) -> anyhow::Result<()> {
2515        let deployer = ProviderBuilder::new()
2516            .wallet(EthereumWallet::from(cfg.signer()))
2517            .connect_http(cfg.l1_url());
2518        StakingTransactions::create(
2519            cfg.l1_url(),
2520            &deployer,
2521            stake_table,
2522            cfg.staking_key_sets(indices),
2523            None,
2524            delegation_config,
2525        )
2526        .await?
2527        .apply_all()
2528        .await?;
2529        Ok(())
2530    }
2531
2532    /// Deregisters the validators at the given node indices, each exit sent
2533    /// from that validator's own funded provider.
2534    pub async fn deregister_validators<const NUM_NODES: usize>(
2535        cfg: &TestConfig<NUM_NODES>,
2536        stake_table: Address,
2537        indices: &[usize],
2538    ) -> anyhow::Result<()> {
2539        let providers = cfg.validator_providers();
2540        for &i in indices {
2541            let (address, provider) = &providers[i];
2542            let receipt = StakingTransaction::DeregisterValidator { stake_table }
2543                .send(provider)
2544                .await?
2545                .get_receipt()
2546                .await?;
2547            anyhow::ensure!(
2548                receipt.status(),
2549                "deregistration of validator {i} ({address}) reverted"
2550            );
2551        }
2552        Ok(())
2553    }
2554
2555    /// Funds a fresh delegator account (ETH via anvil, ESP from the deployer)
2556    /// and delegates `amount` to `validator`. Returns the delegator's provider
2557    /// so the test can later undelegate.
2558    pub async fn delegate_new<const NUM_NODES: usize>(
2559        cfg: &TestConfig<NUM_NODES>,
2560        token: Address,
2561        stake_table: Address,
2562        validator: Address,
2563        amount: U256,
2564    ) -> anyhow::Result<impl Provider + Clone + use<NUM_NODES>> {
2565        let deployer = ProviderBuilder::new()
2566            .wallet(EthereumWallet::from(cfg.signer()))
2567            .connect_http(cfg.l1_url());
2568        let signer = PrivateKeySigner::random();
2569        let delegator = signer.address();
2570        let provider = ProviderBuilder::new()
2571            .wallet(EthereumWallet::from(signer))
2572            .connect_http(cfg.l1_url());
2573
2574        deployer
2575            .anvil_set_balance(delegator, parse_ether("10").unwrap())
2576            .await?;
2577        let funding = StakingTransaction::Transfer {
2578            token,
2579            to: delegator,
2580            amount,
2581        }
2582        .send(&deployer)
2583        .await?
2584        .get_receipt()
2585        .await?;
2586        anyhow::ensure!(funding.status(), "ESP transfer to delegator reverted");
2587
2588        for tx in [
2589            StakingTransaction::Approve {
2590                token,
2591                spender: stake_table,
2592                amount,
2593            },
2594            StakingTransaction::Delegate {
2595                stake_table,
2596                validator,
2597                amount,
2598            },
2599        ] {
2600            let receipt = tx.send(&provider).await?.get_receipt().await?;
2601            anyhow::ensure!(receipt.status(), "delegator transaction reverted");
2602        }
2603        Ok(provider)
2604    }
2605
2606    /// Waits epoch by epoch, starting at `start_epoch`, until the committee
2607    /// reported by `node/validators/{epoch}` satisfies `pred`. Returns the
2608    /// first matching epoch and its committee; panics after `max_epochs`
2609    /// epochs without a match.
2610    pub async fn wait_for_committee(
2611        client: &Client<ClientErr, SequencerApiVersion>,
2612        events: &mut (impl Stream<Item = CoordinatorEvent<SeqTypes>> + Unpin),
2613        epoch_height: u64,
2614        start_epoch: u64,
2615        max_epochs: u64,
2616        pred: impl Fn(&AuthenticatedValidatorMap) -> bool,
2617    ) -> (u64, AuthenticatedValidatorMap) {
2618        let mut last = None;
2619        for epoch in start_epoch..start_epoch + max_epochs {
2620            wait_for_epochs(events, epoch_height, epoch).await;
2621            let validators = client
2622                .get::<AuthenticatedValidatorMap>(&format!("node/validators/{epoch}"))
2623                .send()
2624                .await
2625                .expect("validators for a decided epoch");
2626            if pred(&validators) {
2627                return (epoch, validators);
2628            }
2629            last = Some((epoch, validators));
2630        }
2631        let last =
2632            last.map(|(epoch, validators)| (epoch, validators.keys().copied().collect::<Vec<_>>()));
2633        panic!(
2634            "committee predicate not satisfied within {max_epochs} epochs starting at \
2635             {start_epoch}; last committee: {last:?}"
2636        );
2637    }
2638
2639    /// The L1 accounts of the validators at the given node indices.
2640    pub fn staking_addresses<const NUM_NODES: usize>(
2641        cfg: &TestConfig<NUM_NODES>,
2642        indices: &[usize],
2643    ) -> HashSet<Address> {
2644        cfg.staking_key_sets(indices)
2645            .iter()
2646            .map(|keys| keys.signer.address())
2647            .collect()
2648    }
2649
2650    /// Predicate for [`wait_for_committee`]: the committee is exactly the
2651    /// expected set of validator accounts.
2652    pub fn committee_is(expected: HashSet<Address>) -> impl Fn(&AuthenticatedValidatorMap) -> bool {
2653        move |validators| validators.keys().copied().collect::<HashSet<_>>() == expected
2654    }
2655
2656    /// Asserts the node is live: it must advance `epochs_ahead` epochs (at
2657    /// least 1) past its current decided epoch, and, when the chain runs the
2658    /// self-building new protocol, sequence a newly submitted transaction.
2659    /// Inclusion is not asserted on legacy versions because the test-only
2660    /// legacy builder stops producing non-empty blocks after roughly a
2661    /// hundred views, independent of any stake table activity.
2662    pub async fn assert_node_live<P: SequencerPersistence>(
2663        node: &SequencerContext<network::Memory, P>,
2664        epoch_height: u64,
2665        epochs_ahead: u64,
2666    ) {
2667        assert!(epochs_ahead > 0, "epochs_ahead must be at least 1");
2668        let mut events = node.event_stream();
2669        let leaf = node.decided_leaf().await;
2670        let current = leaf
2671            .epoch(epoch_height)
2672            .map(|epoch| epoch.u64())
2673            .unwrap_or_default();
2674        // `wait_for_epochs` returns on the first epoch strictly greater than
2675        // its target.
2676        wait_for_epochs(&mut events, epoch_height, current + epochs_ahead - 1).await;
2677
2678        if node.decided_leaf().await.block_header().version() < versions::NEW_PROTOCOL_VERSION {
2679            tracing::info!("legacy version: skipping transaction-inclusion liveness check");
2680            return;
2681        }
2682        // Detect inclusion via the header's namespace table: the namespace is
2683        // unique to this call, and decide events at 0.6 do not always carry
2684        // payloads.
2685        static NAMESPACE_COUNTER: AtomicU32 = AtomicU32::new(10_101);
2686        let namespace = NamespaceId::from(NAMESPACE_COUNTER.fetch_add(1, Ordering::Relaxed));
2687        let tx = Transaction::new(namespace, vec![7; 8]);
2688        node.submit_transaction(tx)
2689            .await
2690            .expect("live node accepts transactions");
2691        tokio::time::timeout(Duration::from_secs(120), async {
2692            loop {
2693                let leaf = match events.next().await.unwrap() {
2694                    CoordinatorEvent::LegacyEvent(Event {
2695                        event: EventType::Decide { leaf_chain, .. },
2696                        ..
2697                    }) => leaf_chain[0].leaf.clone(),
2698                    CoordinatorEvent::NewDecide { leaf_infos, .. } => leaf_infos[0].leaf.clone(),
2699                    _ => continue,
2700                };
2701                if leaf
2702                    .block_header()
2703                    .ns_table()
2704                    .find_ns_id(&namespace)
2705                    .is_some()
2706                {
2707                    tracing::info!(height = leaf.height(), "transaction namespace sequenced");
2708                    return;
2709                }
2710            }
2711        })
2712        .await
2713        .expect("submitted transaction was not sequenced in time");
2714    }
2715
2716    /// Asserts every node has decided at least `min_height`, and that nodes
2717    /// which have decided the same height agree on the leaf.
2718    pub async fn assert_nodes_agree<P: SequencerPersistence>(
2719        nodes: &[&SequencerContext<network::Memory, P>],
2720        min_height: u64,
2721    ) {
2722        let leaves = join_all(nodes.iter().map(|node| node.decided_leaf())).await;
2723        let mut by_height: std::collections::BTreeMap<u64, Commitment<Leaf2>> = Default::default();
2724        for (i, leaf) in leaves.iter().enumerate() {
2725            assert!(
2726                leaf.height() >= min_height,
2727                "node {i} decided height {} is below {min_height}",
2728                leaf.height()
2729            );
2730            if let Some(other) = by_height.insert(leaf.height(), leaf.commit()) {
2731                assert_eq!(
2732                    other,
2733                    leaf.commit(),
2734                    "decided-leaf divergence at height {}",
2735                    leaf.height()
2736                );
2737            }
2738        }
2739    }
2740
2741    /// Test the status API with custom options.
2742    ///
2743    /// The `opt` function can be used to modify the [`Options`] which are used to start the server.
2744    /// By default, the options are the minimal required to run this test (configuring a port and
2745    /// enabling the status API). `opt` may add additional functionality (e.g. adding a query module
2746    /// to test a different initialization path) but should not remove or modify the existing
2747    /// functionality (e.g. removing the status module or changing the port).
2748    pub async fn status_test_helper(opt: impl FnOnce(Options) -> Options) {
2749        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
2750        let url = format!("http://localhost:{port}").parse().unwrap();
2751        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
2752
2753        let options = opt(Options::with_port(port));
2754        let network_config = TestConfigBuilder::default().build();
2755        let config = TestNetworkConfigBuilder::default()
2756            .api_config(options)
2757            .network_config(network_config)
2758            .build();
2759        let network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
2760        client.connect(None).await;
2761
2762        // The status API is well tested in the query service repo. Here we are just smoke testing
2763        // that we set it up correctly. Wait for a (non-genesis) block to be sequenced and then
2764        // check the success rate metrics.
2765        while client
2766            .get::<u64>("status/block-height")
2767            .send()
2768            .await
2769            .unwrap()
2770            <= 1
2771        {
2772            sleep(Duration::from_secs(1)).await;
2773        }
2774        let success_rate = client
2775            .get::<f64>("status/success-rate")
2776            .send()
2777            .await
2778            .unwrap();
2779        // If metrics are populating correctly, we should get a finite number. If not, we might get
2780        // NaN or infinity due to division by 0.
2781        assert!(success_rate.is_finite(), "{success_rate}");
2782        // We know at least some views have been successful, since we finalized a block.
2783        assert!(success_rate > 0.0, "{success_rate}");
2784
2785        let keys: NodePublicKeys = client.get("status/keys").send().await.unwrap();
2786        let expected = network.server.validator_config();
2787        assert_eq!(keys.consensus_key, expected.public_key);
2788        assert_eq!(keys.state_ver_key, expected.state_public_key);
2789        assert_eq!(
2790            keys.x25519_key,
2791            expected.x25519_keypair.as_ref().map(|kp| kp.public_key())
2792        );
2793        assert_eq!(keys.eth_account, None);
2794
2795        let json: serde_json::Value = client.get("status/keys").send().await.unwrap();
2796        let bls = json["consensus_key"].as_str().unwrap();
2797        assert!(bls.starts_with("BLS_VER_KEY~"), "{bls}");
2798        let schnorr = json["state_ver_key"].as_str().unwrap();
2799        assert!(schnorr.starts_with("SCHNORR_VER_KEY~"), "{schnorr}");
2800        let x25519 = json["x25519_key"].as_str().unwrap();
2801        assert!(x25519.starts_with("X25519_PK~"), "{x25519}");
2802    }
2803
2804    /// Test the submit API with custom options.
2805    ///
2806    /// The `opt` function can be used to modify the [`Options`] which are used to start the server.
2807    /// By default, the options are the minimal required to run this test (configuring a port and
2808    /// enabling the submit API). `opt` may add additional functionality (e.g. adding a query module
2809    /// to test a different initialization path) but should not remove or modify the existing
2810    /// functionality (e.g. removing the submit module or changing the port).
2811    pub async fn submit_test_helper(opt: impl FnOnce(Options) -> Options) {
2812        let txn = Transaction::new(NamespaceId::from(1_u32), vec![1, 2, 3, 4]);
2813
2814        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
2815
2816        let url = format!("http://localhost:{port}").parse().unwrap();
2817        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
2818
2819        let options = opt(Options::with_port(port).submit(Default::default()));
2820        let network_config = TestConfigBuilder::default().build();
2821        let config = TestNetworkConfigBuilder::default()
2822            .api_config(options)
2823            .network_config(network_config)
2824            .build();
2825        let network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
2826        let mut events = network.server.event_stream();
2827
2828        client.connect(None).await;
2829
2830        let hash = client
2831            .post("submit/submit")
2832            .body_json(&txn)
2833            .unwrap()
2834            .send()
2835            .await
2836            .unwrap();
2837        assert_eq!(txn.commit(), hash);
2838
2839        // Wait for a Decide event containing transaction matching the one we sent
2840        wait_for_decide_on_handle(&mut events, &txn).await;
2841    }
2842
2843    /// Test the state signature API.
2844    pub async fn state_signature_test_helper(opt: impl FnOnce(Options) -> Options) {
2845        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
2846
2847        let url = format!("http://localhost:{port}").parse().unwrap();
2848
2849        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
2850
2851        let options = opt(Options::with_port(port));
2852        let network_config = TestConfigBuilder::default().build();
2853        let config = TestNetworkConfigBuilder::default()
2854            .api_config(options)
2855            .network_config(network_config)
2856            .build();
2857        let network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
2858
2859        let mut height: u64;
2860        // Wait for block >=2 appears
2861        // It's waiting for an extra second to make sure that the signature is generated
2862        loop {
2863            height = network.server.decided_leaf().await.height();
2864            sleep(std::time::Duration::from_secs(1)).await;
2865            if height >= 2 {
2866                break;
2867            }
2868        }
2869        // we cannot verify the signature now, because we don't know the stake table
2870        client
2871            .get::<LCV3StateSignatureRequestBody>(&format!("state-signature/block/{height}"))
2872            .send()
2873            .await
2874            .unwrap();
2875    }
2876
2877    /// Test the catchup API with custom options.
2878    ///
2879    /// The `opt` function can be used to modify the [`Options`] which are used to start the server.
2880    /// By default, the options are the minimal required to run this test (configuring a port and
2881    /// enabling the catchup API). `opt` may add additional functionality (e.g. adding a query module
2882    /// to test a different initialization path) but should not remove or modify the existing
2883    /// functionality (e.g. removing the catchup module or changing the port).
2884    pub async fn catchup_test_helper(opt: impl FnOnce(Options) -> Options) {
2885        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
2886        let url = format!("http://localhost:{port}").parse().unwrap();
2887        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
2888
2889        let options = opt(Options::with_port(port));
2890        let network_config = TestConfigBuilder::default().build();
2891        let config = TestNetworkConfigBuilder::default()
2892            .api_config(options)
2893            .network_config(network_config)
2894            .build();
2895        let network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
2896        client.connect(None).await;
2897
2898        // Wait for a few blocks to be decided.
2899        let mut events = network.server.event_stream();
2900        loop {
2901            if let CoordinatorEvent::LegacyEvent(Event {
2902                event: EventType::Decide { leaf_chain, .. },
2903                ..
2904            }) = events.next().await.unwrap()
2905                && leaf_chain
2906                    .iter()
2907                    .any(|LeafInfo { leaf, .. }| leaf.block_header().height() > 2)
2908            {
2909                break;
2910            }
2911        }
2912
2913        // Stop consensus running on the node so we freeze the decided and undecided states.
2914        // We'll let it go out of scope here since it's a write lock.
2915        {
2916            network.server.shutdown_consensus().await;
2917        }
2918
2919        // Undecided fee state: absent account.
2920        let leaf = network.server.decided_leaf().await;
2921        let height = leaf.height() + 1;
2922        let view = leaf.view_number() + 1;
2923        let res = client
2924            .get::<AccountQueryData>(&format!(
2925                "catchup/{height}/{}/account/{:x}",
2926                view.u64(),
2927                Address::default()
2928            ))
2929            .send()
2930            .await
2931            .unwrap();
2932        assert_eq!(res.balance, U256::ZERO);
2933        assert_eq!(
2934            res.proof
2935                .verify(
2936                    &network
2937                        .server
2938                        .state(view)
2939                        .await
2940                        .unwrap()
2941                        .fee_merkle_tree
2942                        .commitment()
2943                )
2944                .unwrap(),
2945            U256::ZERO,
2946        );
2947
2948        // Undecided block state.
2949        let res = client
2950            .get::<BlocksFrontier>(&format!("catchup/{height}/{}/blocks", view.u64()))
2951            .send()
2952            .await
2953            .unwrap();
2954        let root = &network
2955            .server
2956            .state(view)
2957            .await
2958            .unwrap()
2959            .block_merkle_tree
2960            .commitment();
2961        BlockMerkleTree::verify(root, root.size() - 1, res)
2962            .unwrap()
2963            .unwrap();
2964    }
2965}
2966
2967#[cfg(test)]
2968mod api_tests {
2969    use std::{fmt::Debug, marker::PhantomData};
2970
2971    use committable::Committable;
2972    use data_source::testing::TestableSequencerDataSource;
2973    use espresso_types::{
2974        Header, Leaf2, MOCK_SEQUENCER_VERSIONS, NamespaceId, NamespaceProofQueryData,
2975        ValidatedState,
2976        traits::{EventConsumer, PersistenceOptions},
2977    };
2978    use futures::{future, stream::StreamExt};
2979    use hotshot_example_types::node_types::TEST_VERSIONS;
2980    use hotshot_query_service::availability::{
2981        AvailabilityDataSource, BlockQueryData, VidCommonQueryData,
2982    };
2983    use hotshot_types::{
2984        data::{
2985            DaProposal2, EpochNumber, QuorumProposal2, QuorumProposalWrapper, VidCommitment,
2986            VidDisperseShare, ns_table::parse_ns_table, vid_disperse::AvidMDisperseShare,
2987        },
2988        event::LeafInfo,
2989        message::Proposal,
2990        simple_certificate::{CertificatePair, QuorumCertificate2},
2991        traits::{EncodeBytes, signature_key::SignatureKey},
2992        utils::EpochTransitionIndicator,
2993        vid::avidm::{AvidMScheme, init_avidm_param},
2994    };
2995    use http_client::{Client, error::ClientErr};
2996    use test_helpers::{
2997        TestNetwork, TestNetworkConfigBuilder, catchup_test_helper, state_signature_test_helper,
2998        status_test_helper, submit_test_helper,
2999    };
3000    use test_utils::reserve_tcp_port;
3001    use vbs::version::StaticVersion;
3002
3003    use super::{update::ApiEventConsumer, *};
3004    use crate::{
3005        network,
3006        persistence::no_storage::NoStorage,
3007        testing::{TestConfigBuilder, wait_for_decide_on_handle},
3008    };
3009
3010    #[rstest_reuse::template]
3011    #[rstest::rstest]
3012    #[case(PhantomData::<crate::api::sql::DataSource>)]
3013    #[case(PhantomData::<crate::api::fs::DataSource>)]
3014    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3015    pub fn testable_sequencer_data_source<D: TestableSequencerDataSource>(
3016        #[case] _d: PhantomData<D>,
3017    ) {
3018    }
3019
3020    #[rstest_reuse::apply(testable_sequencer_data_source)]
3021    pub(crate) async fn submit_test_with_query_module<D: TestableSequencerDataSource>(
3022        _d: PhantomData<D>,
3023    ) {
3024        let storage = D::create_storage().await;
3025        submit_test_helper(|opt| D::options(&storage, opt)).await
3026    }
3027
3028    #[rstest_reuse::apply(testable_sequencer_data_source)]
3029    pub(crate) async fn status_test_with_query_module<D: TestableSequencerDataSource>(
3030        _d: PhantomData<D>,
3031    ) {
3032        let storage = D::create_storage().await;
3033        status_test_helper(|opt| D::options(&storage, opt)).await
3034    }
3035
3036    #[rstest_reuse::apply(testable_sequencer_data_source)]
3037    pub(crate) async fn state_signature_test_with_query_module<D: TestableSequencerDataSource>(
3038        _d: PhantomData<D>,
3039    ) {
3040        let storage = D::create_storage().await;
3041        state_signature_test_helper(|opt| D::options(&storage, opt)).await
3042    }
3043
3044    #[rstest_reuse::apply(testable_sequencer_data_source)]
3045    pub(crate) async fn test_namespace_query<D: TestableSequencerDataSource>(_d: PhantomData<D>) {
3046        // Arbitrary transaction, arbitrary namespace ID
3047        let ns_id = NamespaceId::from(42_u32);
3048        let txn = Transaction::new(ns_id, vec![1, 2, 3, 4]);
3049
3050        // Start query service.
3051        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
3052        let storage = D::create_storage().await;
3053        let network_config = TestConfigBuilder::default().build();
3054        let config = TestNetworkConfigBuilder::default()
3055            .api_config(D::options(&storage, Options::with_port(port)).submit(Default::default()))
3056            .network_config(network_config)
3057            .build();
3058        let network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
3059        let mut events = network.server.event_stream();
3060
3061        // Connect client.
3062        let client: Client<ClientErr, StaticVersion<0, 1>> =
3063            Client::new(format!("http://localhost:{port}").parse().unwrap());
3064        client.connect(None).await;
3065
3066        let hash = client
3067            .post("submit/submit")
3068            .body_json(&txn)
3069            .unwrap()
3070            .send()
3071            .await
3072            .unwrap();
3073        assert_eq!(txn.commit(), hash);
3074
3075        // Wait for a Decide event containing transaction matching the one we sent
3076        let block_height = wait_for_decide_on_handle(&mut events, &txn).await.0 as usize;
3077        tracing::info!(block_height, "transaction sequenced");
3078
3079        // Submit a second transaction for range queries.
3080        let txn2 = Transaction::new(ns_id, vec![5, 6, 7, 8]);
3081        client
3082            .post::<Commitment<Transaction>>("submit/submit")
3083            .body_json(&txn2)
3084            .unwrap()
3085            .send()
3086            .await
3087            .unwrap();
3088        let block_height2 = wait_for_decide_on_handle(&mut events, &txn2).await.0 as usize;
3089        tracing::info!(block_height2, "transaction sequenced");
3090
3091        // Wait for the query service to update to this block height.
3092        client
3093            .socket(&format!("availability/stream/blocks/{block_height2}"))
3094            .subscribe::<BlockQueryData<SeqTypes>>()
3095            .await
3096            .unwrap()
3097            .next()
3098            .await
3099            .unwrap()
3100            .unwrap();
3101
3102        let mut found_txn = false;
3103        let mut found_empty_block = false;
3104        for block_num in 0..=block_height {
3105            let header: Header = client
3106                .get(&format!("availability/header/{block_num}"))
3107                .send()
3108                .await
3109                .unwrap();
3110            let ns_query_res: NamespaceProofQueryData = client
3111                .get(&format!("availability/block/{block_num}/namespace/{ns_id}"))
3112                .send()
3113                .await
3114                .unwrap();
3115
3116            // Check other means of querying the same proof.
3117            assert_eq!(
3118                ns_query_res,
3119                client
3120                    .get(&format!(
3121                        "availability/block/hash/{}/namespace/{ns_id}",
3122                        header.commit()
3123                    ))
3124                    .send()
3125                    .await
3126                    .unwrap()
3127            );
3128            assert_eq!(
3129                ns_query_res,
3130                client
3131                    .get(&format!(
3132                        "availability/block/payload-hash/{}/namespace/{ns_id}",
3133                        header.payload_commitment()
3134                    ))
3135                    .send()
3136                    .await
3137                    .unwrap()
3138            );
3139
3140            // Verify namespace proof if present
3141            if let Some(ns_proof) = ns_query_res.proof {
3142                let vid_common: VidCommonQueryData<SeqTypes> = client
3143                    .get(&format!("availability/vid/common/{block_num}"))
3144                    .send()
3145                    .await
3146                    .unwrap();
3147                ns_proof
3148                    .verify(
3149                        header.ns_table(),
3150                        &header.payload_commitment(),
3151                        vid_common.common(),
3152                    )
3153                    .unwrap();
3154            } else {
3155                // Namespace proof should be present if ns_id exists in ns_table
3156                assert!(header.ns_table().find_ns_id(&ns_id).is_none());
3157                assert!(ns_query_res.transactions.is_empty());
3158            }
3159
3160            found_empty_block = found_empty_block || ns_query_res.transactions.is_empty();
3161
3162            for txn in ns_query_res.transactions {
3163                if txn.commit() == hash {
3164                    // Ensure that we validate an inclusion proof
3165                    found_txn = true;
3166                }
3167            }
3168        }
3169        assert!(found_txn);
3170        assert!(found_empty_block);
3171
3172        // Test range query.
3173        let ns_proofs: Vec<NamespaceProofQueryData> = client
3174            .get(&format!(
3175                "availability/block/{block_height}/{}/namespace/{ns_id}",
3176                block_height2 + 1
3177            ))
3178            .send()
3179            .await
3180            .unwrap();
3181        assert_eq!(ns_proofs.len(), block_height2 + 1 - block_height);
3182        assert_eq!(&ns_proofs[0].transactions, std::slice::from_ref(&txn));
3183        assert_eq!(
3184            &ns_proofs[ns_proofs.len() - 1].transactions,
3185            std::slice::from_ref(&txn2)
3186        );
3187        for proof in &ns_proofs[1..ns_proofs.len() - 1] {
3188            assert_eq!(proof.transactions, &[]);
3189        }
3190    }
3191
3192    #[rstest_reuse::apply(testable_sequencer_data_source)]
3193    pub(crate) async fn catchup_test_with_query_module<D: TestableSequencerDataSource>(
3194        _d: PhantomData<D>,
3195    ) {
3196        let storage = D::create_storage().await;
3197        catchup_test_helper(|opt| D::options(&storage, opt)).await
3198    }
3199
3200    #[rstest_reuse::apply(testable_sequencer_data_source)]
3201    pub async fn test_non_consecutive_decide_with_failing_event_consumer<D>(_d: PhantomData<D>)
3202    where
3203        D: TestableSequencerDataSource + Debug + 'static,
3204    {
3205        use hotshot_types::new_protocol::CoordinatorEvent;
3206
3207        #[derive(Clone, Copy, Debug)]
3208        struct FailConsumer;
3209
3210        #[async_trait]
3211        impl EventConsumer for FailConsumer {
3212            async fn handle_event(&self, _: &CoordinatorEvent<SeqTypes>) -> anyhow::Result<()> {
3213                bail!("mock error injection");
3214            }
3215        }
3216
3217        let (pubkey, privkey) = PubKey::generated_from_seed_indexed([0; 32], 1);
3218
3219        let storage = D::create_storage().await;
3220        let persistence = D::persistence_options(&storage).create().await.unwrap();
3221        let data_source: Arc<StorageState<network::Memory, NoStorage, _>> =
3222            Arc::new(StorageState::new(
3223                D::create(D::persistence_options(&storage), Default::default(), false)
3224                    .await
3225                    .unwrap(),
3226                ApiState::new(future::pending()),
3227            ));
3228
3229        // Create two non-consecutive leaf chains.
3230        let mut chain1 = vec![];
3231
3232        let genesis = Leaf2::genesis(
3233            &Default::default(),
3234            &NodeState::mock(),
3235            TEST_VERSIONS.test.base,
3236        )
3237        .await;
3238        let payload = genesis.block_payload().unwrap();
3239        let payload_bytes_arc = payload.encode();
3240
3241        let avidm_param = init_avidm_param(2).unwrap();
3242        let weights = vec![1u32; 2];
3243
3244        let ns_table = parse_ns_table(payload.byte_len().as_usize(), &payload.ns_table().encode());
3245        let (payload_commitment, shares) =
3246            AvidMScheme::ns_disperse(&avidm_param, &weights, &payload_bytes_arc, ns_table).unwrap();
3247
3248        let mut quorum_proposal = QuorumProposalWrapper::<SeqTypes> {
3249            proposal: QuorumProposal2::<SeqTypes> {
3250                block_header: genesis.block_header().clone(),
3251                view_number: ViewNumber::genesis(),
3252                justify_qc: QuorumCertificate2::genesis(
3253                    &ValidatedState::default(),
3254                    &NodeState::mock(),
3255                    MOCK_SEQUENCER_VERSIONS,
3256                )
3257                .await,
3258                upgrade_certificate: None,
3259                view_change_evidence: None,
3260                next_drb_result: None,
3261                next_epoch_justify_qc: None,
3262                epoch: None,
3263                state_cert: None,
3264            },
3265        };
3266        let mut qc = QuorumCertificate2::genesis(
3267            &ValidatedState::default(),
3268            &NodeState::mock(),
3269            MOCK_SEQUENCER_VERSIONS,
3270        )
3271        .await;
3272
3273        let mut justify_qc = qc.clone();
3274        for i in 0..5 {
3275            *quorum_proposal.proposal.block_header.height_mut() = i;
3276            quorum_proposal.proposal.view_number = ViewNumber::new(i);
3277            quorum_proposal.proposal.justify_qc = justify_qc;
3278            let leaf = Leaf2::from_quorum_proposal(&quorum_proposal);
3279            qc.view_number = leaf.view_number();
3280            qc.data.leaf_commit = Committable::commit(&leaf);
3281            justify_qc = qc.clone();
3282            chain1.push((leaf.clone(), CertificatePair::non_epoch_change(qc.clone())));
3283
3284            // Include a quorum proposal for each leaf.
3285            let quorum_proposal_signature =
3286                PubKey::sign(&privkey, &bincode::serialize(&quorum_proposal).unwrap())
3287                    .expect("Failed to sign quorum_proposal");
3288            persistence
3289                .append_quorum_proposal2(&Proposal {
3290                    data: quorum_proposal.clone(),
3291                    signature: quorum_proposal_signature,
3292                    _pd: Default::default(),
3293                })
3294                .await
3295                .unwrap();
3296
3297            // Include VID information for each leaf.
3298            let share: VidDisperseShare<SeqTypes> = AvidMDisperseShare {
3299                view_number: leaf.view_number(),
3300                payload_commitment,
3301                share: shares[0].clone(),
3302                recipient_key: pubkey,
3303                epoch: Some(EpochNumber::new(0)),
3304                target_epoch: Some(EpochNumber::new(0)),
3305                common: avidm_param.clone(),
3306            }
3307            .into();
3308
3309            persistence
3310                .append_vid(&share.to_proposal(&privkey).unwrap())
3311                .await
3312                .unwrap();
3313
3314            // Include payload information for each leaf.
3315            let block_payload_signature =
3316                PubKey::sign(&privkey, &payload_bytes_arc).expect("Failed to sign block payload");
3317            let da_proposal_inner = DaProposal2::<SeqTypes> {
3318                encoded_transactions: payload_bytes_arc.clone(),
3319                metadata: payload.ns_table().clone(),
3320                view_number: leaf.view_number(),
3321                epoch: Some(EpochNumber::new(0)),
3322                epoch_transition_indicator: EpochTransitionIndicator::NotInTransition,
3323            };
3324            let da_proposal = Proposal {
3325                data: da_proposal_inner,
3326                signature: block_payload_signature,
3327                _pd: Default::default(),
3328            };
3329            persistence
3330                .append_da2(&da_proposal, VidCommitment::V1(payload_commitment))
3331                .await
3332                .unwrap();
3333        }
3334        // Split into two chains.
3335        let mut chain2 = chain1.split_off(2);
3336        // Make non-consecutive (i.e. we skip a leaf).
3337        chain2.remove(0);
3338
3339        // Decide 2 leaves, but fail in event processing.
3340        let leaf_chain = chain1
3341            .iter()
3342            .map(|(leaf, qc)| (leaf_info(leaf.clone()), qc.clone()))
3343            .collect::<Vec<_>>();
3344        tracing::info!("decide with event handling failure");
3345        persistence
3346            .append_decided_leaves(
3347                ViewNumber::new(1),
3348                leaf_chain.iter().map(|(leaf, qc)| (leaf, qc.clone())),
3349                None,
3350                &FailConsumer,
3351            )
3352            .await
3353            .unwrap();
3354
3355        // Now decide remaining leaves successfully. We should now process a decide event for all
3356        // the leaves.
3357        let consumer = ApiEventConsumer::from(data_source.clone());
3358        let leaf_chain = chain2
3359            .iter()
3360            .map(|(leaf, qc)| (leaf_info(leaf.clone()), qc.clone()))
3361            .collect::<Vec<_>>();
3362        tracing::info!("decide successfully");
3363        persistence
3364            .append_decided_leaves(
3365                ViewNumber::new(4),
3366                leaf_chain.iter().map(|(leaf, qc)| (leaf, qc.clone())),
3367                None,
3368                &consumer,
3369            )
3370            .await
3371            .unwrap();
3372
3373        // Check that the leaves were moved to archive storage, along with payload and VID
3374        // information.
3375        for (leaf, cert) in chain1.iter().chain(&chain2) {
3376            tracing::info!(height = leaf.height(), "check archive");
3377            let qd = data_source.get_leaf(leaf.height() as usize).await.await;
3378            let stored_leaf: Leaf2 = qd.leaf().clone();
3379            let stored_qc = qd.qc().clone();
3380            assert_eq!(&stored_leaf, leaf);
3381            assert_eq!(&stored_qc, cert.qc());
3382
3383            data_source
3384                .get_block(leaf.height() as usize)
3385                .await
3386                .try_resolve()
3387                .ok()
3388                .unwrap();
3389            data_source
3390                .get_vid_common(leaf.height() as usize)
3391                .await
3392                .try_resolve()
3393                .ok()
3394                .unwrap();
3395
3396            // Check that all data has been garbage collected for the decided views.
3397            assert!(
3398                persistence
3399                    .load_da_proposal(leaf.view_number())
3400                    .await
3401                    .unwrap()
3402                    .is_none()
3403            );
3404            assert!(
3405                persistence
3406                    .load_vid_share(leaf.view_number())
3407                    .await
3408                    .unwrap()
3409                    .is_none()
3410            );
3411            assert!(
3412                persistence
3413                    .load_quorum_proposal(leaf.view_number())
3414                    .await
3415                    .is_err()
3416            );
3417        }
3418
3419        // Check that data has _not_ been garbage collected for the missing view.
3420        assert!(
3421            persistence
3422                .load_da_proposal(ViewNumber::new(2))
3423                .await
3424                .unwrap()
3425                .is_some()
3426        );
3427        assert!(
3428            persistence
3429                .load_vid_share(ViewNumber::new(2))
3430                .await
3431                .unwrap()
3432                .is_some()
3433        );
3434        persistence
3435            .load_quorum_proposal(ViewNumber::new(2))
3436            .await
3437            .unwrap();
3438    }
3439
3440    #[rstest_reuse::apply(testable_sequencer_data_source)]
3441    pub async fn test_decide_missing_data<D>(_d: PhantomData<D>)
3442    where
3443        D: TestableSequencerDataSource + Debug + 'static,
3444    {
3445        use ark_serialize::CanonicalDeserialize;
3446
3447        let storage = D::create_storage().await;
3448        let persistence = D::persistence_options(&storage).create().await.unwrap();
3449        let data_source: Arc<StorageState<network::Memory, NoStorage, _>> =
3450            Arc::new(StorageState::new(
3451                D::create(D::persistence_options(&storage), Default::default(), false)
3452                    .await
3453                    .unwrap(),
3454                ApiState::new(future::pending()),
3455            ));
3456        let consumer = ApiEventConsumer::from(data_source.clone());
3457
3458        let mut qc = QuorumCertificate2::genesis(
3459            &ValidatedState::default(),
3460            &NodeState::mock(),
3461            MOCK_SEQUENCER_VERSIONS,
3462        )
3463        .await;
3464        let leaf = Leaf2::genesis(
3465            &ValidatedState::default(),
3466            &NodeState::mock(),
3467            TEST_VERSIONS.test.base,
3468        )
3469        .await;
3470
3471        // Append the genesis leaf. We don't use this for the test, because the update function will
3472        // automatically fill in the missing data for genesis. We just append this to get into a
3473        // consistent state to then append the leaf from view 1, which will have missing data.
3474        tracing::info!(?leaf, ?qc, "decide genesis leaf");
3475        persistence
3476            .append_decided_leaves(
3477                leaf.view_number(),
3478                [(
3479                    &leaf_info(leaf.clone()),
3480                    CertificatePair::non_epoch_change(qc.clone()),
3481                )],
3482                None,
3483                &consumer,
3484            )
3485            .await
3486            .unwrap();
3487
3488        // Create another leaf, with missing data. We have to use a different payload commitment,
3489        // otherwise the database will be able to combine the empty payload from the genesis block
3490        // with this header, and the payload will not actually be missing.
3491        let mut block_header = leaf.block_header().clone();
3492        *block_header.height_mut() += 1;
3493        *block_header.payload_commitment_mut() = VidCommitment::V1(
3494            CanonicalDeserialize::deserialize_uncompressed_unchecked([1u8; 32].as_slice()).unwrap(),
3495        );
3496        let qp = QuorumProposalWrapper {
3497            proposal: QuorumProposal2 {
3498                block_header,
3499                view_number: leaf.view_number() + 1,
3500                justify_qc: qc.clone(),
3501                upgrade_certificate: None,
3502                view_change_evidence: None,
3503                next_drb_result: None,
3504                next_epoch_justify_qc: None,
3505                epoch: None,
3506                state_cert: None,
3507            },
3508        };
3509
3510        let leaf = Leaf2::from_quorum_proposal(&qp);
3511        qc.view_number = leaf.view_number();
3512        qc.data.leaf_commit = Committable::commit(&leaf);
3513
3514        // Decide a leaf without the corresponding payload or VID.
3515        tracing::info!(?leaf, ?qc, "append leaf 1");
3516        persistence
3517            .append_decided_leaves(
3518                leaf.view_number(),
3519                [(
3520                    &leaf_info(leaf.clone()),
3521                    CertificatePair::non_epoch_change(qc),
3522                )],
3523                None,
3524                &consumer,
3525            )
3526            .await
3527            .unwrap();
3528
3529        // Check that we still processed the leaf.
3530        assert_eq!(leaf, data_source.get_leaf(1).await.await.leaf().clone());
3531        assert!(data_source.get_vid_common(1).await.is_pending());
3532        assert!(data_source.get_block(1).await.is_pending());
3533    }
3534
3535    fn leaf_info(leaf: Leaf2) -> LeafInfo<SeqTypes> {
3536        LeafInfo {
3537            leaf,
3538            vid_share: None,
3539            state: Default::default(),
3540            delta: None,
3541            state_cert: None,
3542        }
3543    }
3544}
3545
3546#[cfg(test)]
3547mod test {
3548    use std::{
3549        collections::{HashMap, HashSet},
3550        time::{Duration, Instant},
3551    };
3552
3553    use ::light_client::{
3554        consensus::{
3555            header::HeaderProof,
3556            leaf::{FinalityProof, LeafProof, LeafProofHint},
3557            payload::PayloadProof,
3558        },
3559        testing::{EpochChangeQuorum, LEGACY_VERSION},
3560    };
3561    use alloy::{
3562        eips::BlockId,
3563        network::EthereumWallet,
3564        primitives::{Address, U256},
3565        providers::{ProviderBuilder, ext::AnvilApi},
3566    };
3567    use async_lock::Mutex;
3568    use committable::{Commitment, Committable};
3569    use espresso_contract_deployer::{
3570        Contract, Contracts, builder::DeployerArgsBuilder,
3571        network_config::light_client_genesis_from_stake_table, upgrade_stake_table_v2,
3572        upgrade_stake_table_v3,
3573    };
3574    use espresso_types::{
3575        FeeAmount, Header, L1Client, L1ClientOptions, MOCK_SEQUENCER_VERSIONS, NamespaceId,
3576        NamespaceProofQueryData, NsProof, RegisteredValidatorMap, RewardDistributor,
3577        StakeTableState, StateCertQueryDataV1, StateCertQueryDataV2, ValidatedState,
3578        ValidatorLeaderCounts,
3579        config::PublicHotShotConfig,
3580        traits::{NullEventConsumer, PersistenceOptions},
3581        v0_3::{COMMISSION_BASIS_POINTS, Fetcher, RewardAmount, RewardMerkleProofV1},
3582        v0_4::{RewardAccountV2, RewardMerkleProofV2},
3583        validators_from_l1_events,
3584    };
3585    use futures::{
3586        future::{self, join_all, try_join_all},
3587        stream::{StreamExt, TryStreamExt},
3588        try_join,
3589    };
3590    use hotshot::types::{Event, EventType};
3591    use hotshot_contract_adapter::{
3592        reward::RewardClaimInput,
3593        sol_types::{EspToken, StakeTableV3},
3594        stake_table::StakeTableContractVersion,
3595    };
3596    use hotshot_query_service::{
3597        availability::{
3598            BlockQueryData, BlockSummaryQueryData, LeafQueryData, TransactionQueryData,
3599            VidCommonQueryData,
3600        },
3601        data_source::{
3602            VersionedDataSource,
3603            sql::Config,
3604            storage::{SqlStorage, StorageConnectionType},
3605        },
3606        explorer::TransactionSummariesResponse,
3607        types::HeightIndexed,
3608    };
3609    use hotshot_types::{
3610        ValidatorConfig,
3611        addr::NetAddr,
3612        data::EpochNumber,
3613        event::LeafInfo,
3614        new_protocol::CoordinatorEvent,
3615        traits::{block_contents::BlockHeader, election::Membership, metrics::NoMetrics},
3616        utils::epoch_from_block_number,
3617        x25519,
3618    };
3619    use http_client::{
3620        Client, StatusCode,
3621        error::ClientErr,
3622        healthcheck::{AppHealth, HealthStatus},
3623    };
3624    use jf_merkle_tree_compat::{
3625        MerkleTreeScheme,
3626        prelude::{MerkleProof, Sha3Node},
3627    };
3628    use pretty_assertions::assert_matches;
3629    use rand::seq::SliceRandom;
3630    use rstest::rstest;
3631    use staking_cli::{
3632        Transaction as StakingTransaction, demo::DelegationConfig, fetch_commission,
3633        update_commission, update_network_config,
3634    };
3635    use test_helpers::{
3636        TestNetwork, TestNetworkConfigBuilder, catchup_test_helper, state_signature_test_helper,
3637        status_test_helper, submit_test_helper,
3638    };
3639    use test_utils::reserve_tcp_port;
3640    use tokio::time::sleep;
3641    use vbs::version::StaticVersion;
3642    use versions::{
3643        DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION, EPOCH_VERSION, FEE_VERSION,
3644        NEW_PROTOCOL_VERSION, Upgrade, version,
3645    };
3646
3647    use self::{
3648        data_source::testing::TestableSequencerDataSource, options::HotshotEvents,
3649        sql::DataSource as SqlDataSource,
3650    };
3651    use super::*;
3652
3653    async fn wait_until_block_height(
3654        client: &Client<ClientErr, StaticVersion<0, 1>>,
3655        endpoint: &str,
3656        height: u64,
3657    ) {
3658        for _retry in 0.. {
3659            let bh = client
3660                .get::<u64>(endpoint)
3661                .send()
3662                .await
3663                .expect("block height not found");
3664
3665            if bh >= height {
3666                return;
3667            }
3668            sleep(Duration::from_secs(3)).await;
3669        }
3670    }
3671    use crate::{
3672        api::{
3673            options::Query,
3674            sql::{impl_testable_data_source::tmp_options, reconstruct_state},
3675            test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
3676        },
3677        catchup::{NullStateCatchup, StatePeers},
3678        persistence,
3679        persistence::no_storage,
3680        testing::{TestConfig, TestConfigBuilder, wait_for_decide_on_handle, wait_for_epochs},
3681    };
3682
3683    const POS_V3: Upgrade = Upgrade::trivial(version(0, 3));
3684    const POS_V4: Upgrade = Upgrade::trivial(version(0, 4));
3685
3686    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3687    async fn test_healthcheck() {
3688        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
3689        let url = format!("http://localhost:{port}").parse().unwrap();
3690        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
3691        let options = Options::with_port(port);
3692        let network_config = TestConfigBuilder::default().build();
3693        let config = TestNetworkConfigBuilder::<5, _, NullStateCatchup>::default()
3694            .api_config(options)
3695            .network_config(network_config)
3696            .build();
3697        let _network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
3698
3699        client.connect(None).await;
3700        let health = client.get::<AppHealth>("healthcheck").send().await.unwrap();
3701        assert_eq!(health.status, HealthStatus::Available);
3702    }
3703
3704    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3705    async fn status_test_without_query_module() {
3706        status_test_helper(|opt| opt).await
3707    }
3708
3709    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3710    async fn submit_test_without_query_module() {
3711        submit_test_helper(|opt| opt).await
3712    }
3713
3714    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3715    async fn state_signature_test_without_query_module() {
3716        state_signature_test_helper(|opt| opt).await
3717    }
3718
3719    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3720    async fn catchup_test_without_query_module() {
3721        catchup_test_helper(|opt| opt).await
3722    }
3723
3724    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3725    async fn test_leaf_only_data_source() {
3726        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
3727
3728        let storage = SqlDataSource::create_storage().await;
3729        let options =
3730            SqlDataSource::leaf_only_ds_options(&storage, Options::with_port(port)).unwrap();
3731
3732        let network_config = TestConfigBuilder::default().build();
3733        let config = TestNetworkConfigBuilder::default()
3734            .api_config(options)
3735            .network_config(network_config)
3736            .build();
3737        let _network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
3738        let url = format!("http://localhost:{port}").parse().unwrap();
3739        let client: Client<ClientErr, SequencerApiVersion> = Client::new(url);
3740
3741        tracing::info!("waiting for blocks");
3742        client.connect(Some(Duration::from_secs(15))).await;
3743        // Wait until some blocks have been decided.
3744
3745        let account = TestConfig::<5>::builder_key().fee_account();
3746
3747        let _headers = client
3748            .socket("availability/stream/headers/0")
3749            .subscribe::<Header>()
3750            .await
3751            .unwrap()
3752            .take(10)
3753            .try_collect::<Vec<_>>()
3754            .await
3755            .unwrap();
3756
3757        for i in 1..5 {
3758            let leaf = client
3759                .get::<LeafQueryData<SeqTypes>>(&format!("availability/leaf/{i}"))
3760                .send()
3761                .await
3762                .unwrap();
3763
3764            assert_eq!(leaf.height(), i);
3765
3766            let header = client
3767                .get::<Header>(&format!("availability/header/{i}"))
3768                .send()
3769                .await
3770                .unwrap();
3771
3772            assert_eq!(header.height(), i);
3773
3774            let vid = client
3775                .get::<VidCommonQueryData<SeqTypes>>(&format!("availability/vid/common/{i}"))
3776                .send()
3777                .await
3778                .unwrap();
3779
3780            assert_eq!(vid.height(), i);
3781
3782            client
3783                .get::<MerkleProof<Commitment<Header>, u64, Sha3Node, 3>>(&format!(
3784                    "block-state/{i}/{}",
3785                    i - 1
3786                ))
3787                .send()
3788                .await
3789                .unwrap();
3790
3791            client
3792                .get::<MerkleProof<FeeAmount, FeeAccount, Sha3Node, 256>>(&format!(
3793                    "fee-state/{}/{}",
3794                    i + 1,
3795                    account
3796                ))
3797                .send()
3798                .await
3799                .unwrap();
3800        }
3801
3802        // This would fail even though we have processed atleast 10 leaves
3803        // this is because light weight nodes only support leaves, headers and VID
3804        client
3805            .get::<BlockQueryData<SeqTypes>>("availability/block/1")
3806            .send()
3807            .await
3808            .unwrap_err();
3809    }
3810
3811    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3812    async fn test_database_metadata_endpoints() {
3813        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
3814
3815        let storage = SqlDataSource::create_storage().await;
3816        let options = SqlDataSource::options(&storage, Options::with_port(port));
3817
3818        let network_config = TestConfigBuilder::default().build();
3819        let config = TestNetworkConfigBuilder::default()
3820            .api_config(options)
3821            .network_config(network_config)
3822            .build();
3823        let _network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
3824        let url = format!("http://localhost:{port}").parse().unwrap();
3825        let client: Client<ClientErr, SequencerApiVersion> = Client::new(url);
3826        client.connect(Some(Duration::from_secs(15))).await;
3827
3828        let table_sizes = client
3829            .get::<Vec<data_source::TableSize>>("database/table-sizes")
3830            .send()
3831            .await
3832            .unwrap();
3833        assert!(!table_sizes.is_empty());
3834
3835        // Deferred backfill migrations register tracking rows at node startup, so the list may
3836        // be non-empty; just check the entries are well-formed.
3837        let migration_status = client
3838            .get::<Vec<data_source::MigrationStatus>>("database/migration-status")
3839            .send()
3840            .await
3841            .unwrap();
3842        assert!(migration_status.iter().all(|m| !m.name.is_empty()));
3843    }
3844
3845    async fn run_catchup_test(url_suffix: &str) {
3846        // Start a sequencer network, using the query service for catchup.
3847        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
3848        const NUM_NODES: usize = 5;
3849
3850        let url: url::Url = format!("http://localhost:{port}{url_suffix}")
3851            .parse()
3852            .unwrap();
3853
3854        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
3855            .api_config(Options::with_port(port))
3856            .network_config(TestConfigBuilder::default().build())
3857            .catchups(std::array::from_fn(|_| {
3858                StatePeers::<StaticVersion<0, 1>>::from_urls(
3859                    vec![url.clone()],
3860                    Default::default(),
3861                    Duration::from_secs(2),
3862                    &NoMetrics,
3863                )
3864            }))
3865            .build();
3866        let mut network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
3867
3868        // Wait for replica 0 to reach a (non-genesis) decide, before disconnecting it.
3869        let mut events = network.peers[0].event_stream();
3870        loop {
3871            let event = events.next().await.unwrap();
3872            let CoordinatorEvent::LegacyEvent(Event {
3873                event: EventType::Decide { leaf_chain, .. },
3874                ..
3875            }) = event
3876            else {
3877                continue;
3878            };
3879            if leaf_chain[0].leaf.height() > 0 {
3880                break;
3881            }
3882        }
3883
3884        // Shut down and restart replica 0. We don't just stop consensus and restart it; we fully
3885        // drop the node and recreate it so it loses all of its temporary state and starts off from
3886        // genesis. It should be able to catch up by listening to proposals and then rebuild its
3887        // state from its peers.
3888        tracing::info!("shutting down node");
3889        network.peers.remove(0);
3890
3891        // Wait for a few blocks to pass while the node is down, so it falls behind.
3892        network
3893            .server
3894            .event_stream()
3895            .filter(|event| {
3896                future::ready(matches!(
3897                    event,
3898                    CoordinatorEvent::LegacyEvent(Event {
3899                        event: EventType::Decide { .. },
3900                        ..
3901                    })
3902                ))
3903            })
3904            .take(3)
3905            .collect::<Vec<_>>()
3906            .await;
3907
3908        tracing::info!("restarting node");
3909        let node = network
3910            .cfg
3911            .init_node(
3912                1,
3913                ValidatedState::default(),
3914                no_storage::Options,
3915                Some(StatePeers::<StaticVersion<0, 1>>::from_urls(
3916                    vec![url],
3917                    Default::default(),
3918                    Duration::from_secs(2),
3919                    &NoMetrics,
3920                )),
3921                None,
3922                &NoMetrics,
3923                test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
3924                NullEventConsumer,
3925                MOCK_SEQUENCER_VERSIONS,
3926                Default::default(),
3927            )
3928            .await;
3929        let mut events = node.event_stream();
3930
3931        // Wait for a (non-genesis) block proposed by each node, to prove that the lagging node has
3932        // caught up and all nodes are in sync.
3933        let mut proposers = [false; NUM_NODES];
3934        loop {
3935            let event = events.next().await.unwrap();
3936            let CoordinatorEvent::LegacyEvent(Event {
3937                event: EventType::Decide { leaf_chain, .. },
3938                ..
3939            }) = event
3940            else {
3941                continue;
3942            };
3943            for LeafInfo { leaf, .. } in leaf_chain.iter().rev() {
3944                let height = leaf.height();
3945                let leaf_builder = (leaf.view_number().u64() as usize) % NUM_NODES;
3946                if height == 0 {
3947                    continue;
3948                }
3949
3950                tracing::info!(
3951                    "waiting for blocks from {proposers:?}, block {height} is from {leaf_builder}",
3952                );
3953                proposers[leaf_builder] = true;
3954            }
3955
3956            if proposers.iter().all(|has_proposed| *has_proposed) {
3957                break;
3958            }
3959        }
3960    }
3961
3962    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3963    async fn test_catchup() {
3964        run_catchup_test("").await;
3965    }
3966
3967    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3968    async fn test_catchup_v0() {
3969        run_catchup_test("/v0").await;
3970    }
3971
3972    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3973    async fn test_catchup_v1() {
3974        run_catchup_test("/v1").await;
3975    }
3976
3977    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3978    async fn test_catchup_no_state_peers() {
3979        // Start a sequencer network, using the query service for catchup.
3980        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
3981        const NUM_NODES: usize = 5;
3982        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
3983            .api_config(Options::with_port(port))
3984            .network_config(TestConfigBuilder::default().build())
3985            .build();
3986        let mut network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
3987
3988        // Wait for replica 0 to reach a (non-genesis) decide, before disconnecting it.
3989        let mut events = network.peers[0].event_stream();
3990        loop {
3991            let event = events.next().await.unwrap();
3992            let CoordinatorEvent::LegacyEvent(Event {
3993                event: EventType::Decide { leaf_chain, .. },
3994                ..
3995            }) = event
3996            else {
3997                continue;
3998            };
3999            if leaf_chain[0].leaf.height() > 0 {
4000                break;
4001            }
4002        }
4003
4004        // Shut down and restart replica 0. We don't just stop consensus and restart it; we fully
4005        // drop the node and recreate it so it loses all of its temporary state and starts off from
4006        // genesis. It should be able to catch up by listening to proposals and then rebuild its
4007        // state from its peers.
4008        tracing::info!("shutting down node");
4009        network.peers.remove(0);
4010
4011        // Wait for a few blocks to pass while the node is down, so it falls behind.
4012        network
4013            .server
4014            .event_stream()
4015            .filter(|event| {
4016                future::ready(matches!(
4017                    event,
4018                    CoordinatorEvent::LegacyEvent(Event {
4019                        event: EventType::Decide { .. },
4020                        ..
4021                    })
4022                ))
4023            })
4024            .take(3)
4025            .collect::<Vec<_>>()
4026            .await;
4027
4028        tracing::info!("restarting node");
4029        let node = network
4030            .cfg
4031            .init_node(
4032                1,
4033                ValidatedState::default(),
4034                no_storage::Options,
4035                None::<NullStateCatchup>,
4036                None,
4037                &NoMetrics,
4038                test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
4039                NullEventConsumer,
4040                MOCK_SEQUENCER_VERSIONS,
4041                Default::default(),
4042            )
4043            .await;
4044        let mut events = node.event_stream();
4045
4046        // Wait for a (non-genesis) block proposed by each node, to prove that the lagging node has
4047        // caught up and all nodes are in sync.
4048        let mut proposers = [false; NUM_NODES];
4049        loop {
4050            let event = events.next().await.unwrap();
4051            let CoordinatorEvent::LegacyEvent(Event {
4052                event: EventType::Decide { leaf_chain, .. },
4053                ..
4054            }) = event
4055            else {
4056                continue;
4057            };
4058            for LeafInfo { leaf, .. } in leaf_chain.iter().rev() {
4059                let height = leaf.height();
4060                let leaf_builder = (leaf.view_number().u64() as usize) % NUM_NODES;
4061                if height == 0 {
4062                    continue;
4063                }
4064
4065                tracing::info!(
4066                    "waiting for blocks from {proposers:?}, block {height} is from {leaf_builder}",
4067                );
4068                proposers[leaf_builder] = true;
4069            }
4070
4071            if proposers.iter().all(|has_proposed| *has_proposed) {
4072                break;
4073            }
4074        }
4075    }
4076
4077    #[ignore]
4078    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4079    async fn test_catchup_epochs_no_state_peers() {
4080        // Start a sequencer network, using the query service for catchup.
4081        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4082        const EPOCH_HEIGHT: u64 = 5;
4083        let network_config = TestConfigBuilder::default()
4084            .epoch_height(EPOCH_HEIGHT)
4085            .build();
4086        const NUM_NODES: usize = 5;
4087        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
4088            .api_config(Options::with_port(port))
4089            .network_config(network_config)
4090            .build();
4091        let mut network = TestNetwork::new(config, Upgrade::trivial(EPOCH_VERSION)).await;
4092
4093        // Wait for replica 0 to decide in the third epoch.
4094        let mut events = network.peers[0].event_stream();
4095        loop {
4096            let event = events.next().await.unwrap();
4097            let CoordinatorEvent::LegacyEvent(Event {
4098                event: EventType::Decide { leaf_chain, .. },
4099                ..
4100            }) = event
4101            else {
4102                continue;
4103            };
4104            tracing::error!("got decide height {}", leaf_chain[0].leaf.height());
4105
4106            if leaf_chain[0].leaf.height() > EPOCH_HEIGHT * 3 {
4107                tracing::error!("decided past one epoch");
4108                break;
4109            }
4110        }
4111
4112        // Shut down and restart replica 0. We don't just stop consensus and restart it; we fully
4113        // drop the node and recreate it so it loses all of its temporary state and starts off from
4114        // genesis. It should be able to catch up by listening to proposals and then rebuild its
4115        // state from its peers.
4116        tracing::info!("shutting down node");
4117        network.peers.remove(0);
4118
4119        // Wait for a few blocks to pass while the node is down, so it falls behind.
4120        network
4121            .server
4122            .event_stream()
4123            .filter(|event| {
4124                future::ready(matches!(
4125                    event,
4126                    CoordinatorEvent::LegacyEvent(Event {
4127                        event: EventType::Decide { .. },
4128                        ..
4129                    })
4130                ))
4131            })
4132            .take(3)
4133            .collect::<Vec<_>>()
4134            .await;
4135
4136        tracing::error!("restarting node");
4137        let node = network
4138            .cfg
4139            .init_node(
4140                1,
4141                ValidatedState::default(),
4142                no_storage::Options,
4143                None::<NullStateCatchup>,
4144                None,
4145                &NoMetrics,
4146                test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
4147                NullEventConsumer,
4148                MOCK_SEQUENCER_VERSIONS,
4149                Default::default(),
4150            )
4151            .await;
4152        let mut events = node.event_stream();
4153
4154        // Wait for a (non-genesis) block proposed by each node, to prove that the lagging node has
4155        // caught up and all nodes are in sync.
4156        let mut proposers = [false; NUM_NODES];
4157        loop {
4158            let event = events.next().await.unwrap();
4159            let CoordinatorEvent::LegacyEvent(Event {
4160                event: EventType::Decide { leaf_chain, .. },
4161                ..
4162            }) = event
4163            else {
4164                continue;
4165            };
4166            for LeafInfo { leaf, .. } in leaf_chain.iter().rev() {
4167                let height = leaf.height();
4168                let leaf_builder = (leaf.view_number().u64() as usize) % NUM_NODES;
4169                if height == 0 {
4170                    continue;
4171                }
4172
4173                tracing::info!(
4174                    "waiting for blocks from {proposers:?}, block {height} is from {leaf_builder}",
4175                );
4176                proposers[leaf_builder] = true;
4177            }
4178
4179            if proposers.iter().all(|has_proposed| *has_proposed) {
4180                break;
4181            }
4182        }
4183    }
4184
4185    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4186    async fn test_chain_config_from_instance() {
4187        // This test uses a ValidatedState which only has the default chain config commitment.
4188        // The NodeState has the full chain config.
4189        // Both chain config commitments will match, so the ValidatedState should have the
4190        // full chain config after a non-genesis block is decided.
4191
4192        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4193
4194        let chain_config: ChainConfig = ChainConfig::default();
4195
4196        let state = ValidatedState {
4197            chain_config: chain_config.commit().into(),
4198            ..Default::default()
4199        };
4200
4201        let states = std::array::from_fn(|_| state.clone());
4202
4203        let config = TestNetworkConfigBuilder::default()
4204            .api_config(Options::with_port(port))
4205            .states(states)
4206            .catchups(std::array::from_fn(|_| {
4207                StatePeers::<StaticVersion<0, 1>>::from_urls(
4208                    vec![format!("http://localhost:{port}").parse().unwrap()],
4209                    Default::default(),
4210                    Duration::from_secs(2),
4211                    &NoMetrics,
4212                )
4213            }))
4214            .network_config(TestConfigBuilder::default().build())
4215            .build();
4216
4217        let mut network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
4218
4219        // Wait for few blocks to be decided.
4220        network
4221            .server
4222            .event_stream()
4223            .filter(|event| {
4224                future::ready(matches!(
4225                    event,
4226                    CoordinatorEvent::LegacyEvent(Event {
4227                        event: EventType::Decide { .. },
4228                        ..
4229                    })
4230                ))
4231            })
4232            .take(3)
4233            .collect::<Vec<_>>()
4234            .await;
4235
4236        for peer in &network.peers {
4237            let state = peer.consensus_handle().decided_state().await.unwrap();
4238
4239            assert_eq!(state.chain_config.resolve().unwrap(), chain_config)
4240        }
4241
4242        network.server.shut_down().await;
4243        drop(network);
4244    }
4245
4246    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4247    async fn test_chain_config_catchup() {
4248        // This test uses a ValidatedState with a non-default chain config
4249        // so it will be different from the NodeState chain config used by the TestNetwork.
4250        // However, for this test to work, at least one node should have a full chain config
4251        // to allow other nodes to catch up.
4252
4253        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4254
4255        let cf = ChainConfig {
4256            max_block_size: 300.into(),
4257            base_fee: 1.into(),
4258            ..Default::default()
4259        };
4260
4261        // State1 contains only the chain config commitment
4262        let state1 = ValidatedState {
4263            chain_config: cf.commit().into(),
4264            ..Default::default()
4265        };
4266
4267        //state 2 contains the full chain config
4268        let state2 = ValidatedState {
4269            chain_config: cf.into(),
4270            ..Default::default()
4271        };
4272
4273        let mut states = std::array::from_fn(|_| state1.clone());
4274        // only one node has the full chain config
4275        // all the other nodes should do a catchup to get the full chain config from peer 0
4276        states[0] = state2;
4277
4278        const NUM_NODES: usize = 5;
4279        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
4280            .api_config(Options::from(options::Http {
4281                port,
4282                max_connections: None,
4283                tonic_port: None,
4284            }))
4285            .states(states)
4286            .catchups(std::array::from_fn(|_| {
4287                StatePeers::<StaticVersion<0, 1>>::from_urls(
4288                    vec![format!("http://localhost:{port}").parse().unwrap()],
4289                    Default::default(),
4290                    Duration::from_secs(2),
4291                    &NoMetrics,
4292                )
4293            }))
4294            .network_config(TestConfigBuilder::default().build())
4295            .build();
4296
4297        let mut network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
4298
4299        // Wait for a few blocks to be decided.
4300        network
4301            .server
4302            .event_stream()
4303            .filter(|event| {
4304                future::ready(matches!(
4305                    event,
4306                    CoordinatorEvent::LegacyEvent(Event {
4307                        event: EventType::Decide { .. },
4308                        ..
4309                    })
4310                ))
4311            })
4312            .take(3)
4313            .collect::<Vec<_>>()
4314            .await;
4315
4316        for peer in &network.peers {
4317            let state = peer.consensus_handle().decided_state().await.unwrap();
4318
4319            assert_eq!(state.chain_config.resolve().unwrap(), cf)
4320        }
4321
4322        network.server.shut_down().await;
4323        drop(network);
4324    }
4325
4326    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4327    async fn test_pos_upgrade_view_based() {
4328        test_upgrade_helper(Upgrade::new(FEE_VERSION, EPOCH_VERSION)).await;
4329    }
4330
4331    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4332    async fn test_epoch_reward_upgrade() {
4333        // Use fewer nodes: epoch mode from view 0 is resource-heavy on CI with
4334        // postgres Docker containers, causing view timeouts and consensus stall.
4335        test_upgrade_helper_with_nodes::<3>(
4336            Upgrade::new(
4337                versions::DRB_AND_HEADER_UPGRADE_VERSION,
4338                versions::EPOCH_REWARD_VERSION,
4339            ),
4340            100,
4341        )
4342        .await;
4343    }
4344
4345    async fn test_upgrade_helper(upgrade: Upgrade) {
4346        test_upgrade_helper_with_nodes::<5>(upgrade, 200).await;
4347    }
4348
4349    async fn test_upgrade_helper_with_nodes<const NUM_NODES: usize>(
4350        upgrade: Upgrade,
4351        start_proposing_view: u64,
4352    ) {
4353        // wait this number of views beyond the configured first view
4354        // before asserting anything.
4355        let wait_extra_views = 10;
4356        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4357        let epoch_start_block = if upgrade.base >= versions::EPOCH_VERSION {
4358            0
4359        } else {
4360            321
4361        };
4362
4363        let test_config = TestConfigBuilder::default()
4364            .epoch_height(200)
4365            .epoch_start_block(epoch_start_block)
4366            .set_upgrades(upgrade.target)
4367            .await
4368            .upgrade_proposing_views(start_proposing_view, 1000)
4369            .build();
4370
4371        let chain_config_genesis = ValidatedState::default().chain_config.resolve().unwrap();
4372        let chain_config_upgrade = test_config.get_upgrade_map().chain_config(upgrade.target);
4373        assert_ne!(chain_config_genesis, chain_config_upgrade);
4374        tracing::debug!(?chain_config_genesis, ?chain_config_upgrade);
4375
4376        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
4377        let persistence: [_; NUM_NODES] = storage
4378            .iter()
4379            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
4380            .collect::<Vec<_>>()
4381            .try_into()
4382            .unwrap();
4383
4384        let mut builder = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
4385            .api_config(SqlDataSource::options(
4386                &storage[0],
4387                Options::with_port(port),
4388            ))
4389            .persistences(persistence)
4390            .catchups(std::array::from_fn(|_| {
4391                StatePeers::<SequencerApiVersion>::from_urls(
4392                    vec![format!("http://localhost:{port}").parse().unwrap()],
4393                    Default::default(),
4394                    Duration::from_secs(2),
4395                    &NoMetrics,
4396                )
4397            }))
4398            .network_config(test_config);
4399
4400        // When the base version already has epochs, the base chain config must
4401        // include the stake_table_contract
4402        if upgrade.base >= versions::EPOCH_VERSION {
4403            let state = ValidatedState {
4404                chain_config: chain_config_upgrade.into(),
4405                ..Default::default()
4406            };
4407            builder = builder.states(std::array::from_fn(|_| state.clone()));
4408        }
4409
4410        let config = builder.build();
4411
4412        let mut network = TestNetwork::new(config, upgrade).await;
4413        let _events = network.server.event_stream();
4414
4415        let target = upgrade.target;
4416
4417        // First loop to get an `UpgradeProposal`. Note that the
4418        // actual upgrade will take several to many subsequent views for
4419        // voting and finally the actual upgrade.
4420        // Use the raw HotShot event stream for upgrade testing, since
4421        // UpgradeProposal events are HotShot-specific and not surfaced
4422        // through the CoordinatorEvent adapter.
4423        let mut hotshot_events = network
4424            .server
4425            .consensus_handle()
4426            .legacy_consensus()
4427            .read()
4428            .await
4429            .event_stream();
4430        let upgrade = loop {
4431            let event = hotshot_events.next().await.unwrap();
4432            if let EventType::UpgradeProposal { proposal, .. } = event.event {
4433                tracing::info!(?proposal, "proposal");
4434                let upgrade = proposal.data.upgrade_proposal;
4435                let new_version = upgrade.new_version;
4436                tracing::info!(?new_version, "upgrade proposal new version");
4437                assert_eq!(new_version, target);
4438                break upgrade;
4439            }
4440        };
4441
4442        let wanted_view = upgrade.new_version_first_view + wait_extra_views;
4443        // Loop until we get the `new_version_first_view`, then test the upgrade.
4444        loop {
4445            let event = hotshot_events.next().await.unwrap();
4446            let view_number = event.view_number;
4447
4448            tracing::debug!(?view_number, ?upgrade.new_version_first_view, "upgrade_new_view");
4449            if view_number > wanted_view {
4450                tracing::info!(?view_number, ?upgrade.new_version_first_view, "passed upgrade view");
4451                let states =
4452                    join_all(network.peers.iter().map(|peer| async {
4453                        peer.consensus_handle().decided_state().await.unwrap()
4454                    }))
4455                    .await;
4456                let leaves = join_all(
4457                    network
4458                        .peers
4459                        .iter()
4460                        .map(|peer| async { peer.consensus_handle().decided_leaf().await }),
4461                )
4462                .await;
4463                let configs: Vec<ChainConfig> = states
4464                    .iter()
4465                    .map(|state| state.chain_config.resolve().unwrap())
4466                    .collect();
4467
4468                tracing::info!(?leaves, ?configs, "post upgrade state");
4469                for config in configs {
4470                    assert_eq!(config, chain_config_upgrade);
4471                }
4472                for leaf in leaves {
4473                    assert_eq!(leaf.block_header().version(), target);
4474                }
4475                break;
4476            }
4477            sleep(Duration::from_millis(200)).await;
4478        }
4479
4480        network.server.shut_down().await;
4481    }
4482
4483    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4484    pub(crate) async fn test_restart() {
4485        const NUM_NODES: usize = 5;
4486        // Initialize nodes.
4487        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
4488        let persistence: [_; NUM_NODES] = storage
4489            .iter()
4490            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
4491            .collect::<Vec<_>>()
4492            .try_into()
4493            .unwrap();
4494        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4495        let config = TestNetworkConfigBuilder::default()
4496            .api_config(SqlDataSource::options(
4497                &storage[0],
4498                Options::with_port(port),
4499            ))
4500            .persistences(persistence.clone())
4501            .network_config(TestConfigBuilder::default().build())
4502            .build();
4503        let mut network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
4504
4505        // Connect client.
4506        let client: Client<ClientErr, SequencerApiVersion> =
4507            Client::new(format!("http://localhost:{port}").parse().unwrap());
4508        client.connect(None).await;
4509        tracing::info!(port, "server running");
4510
4511        // Wait until some blocks have been decided.
4512        client
4513            .socket("availability/stream/blocks/0")
4514            .subscribe::<BlockQueryData<SeqTypes>>()
4515            .await
4516            .unwrap()
4517            .take(3)
4518            .collect::<Vec<_>>()
4519            .await;
4520
4521        // Shut down the consensus nodes.
4522        tracing::info!("shutting down nodes");
4523        network.stop_consensus().await;
4524
4525        // Get the block height we reached.
4526        let height = client
4527            .get::<usize>("status/block-height")
4528            .send()
4529            .await
4530            .unwrap();
4531        tracing::info!("decided {height} blocks before shutting down");
4532
4533        // Get the decided chain, so we can check consistency after the restart.
4534        let chain: Vec<LeafQueryData<SeqTypes>> = client
4535            .socket("availability/stream/leaves/0")
4536            .subscribe()
4537            .await
4538            .unwrap()
4539            .take(height)
4540            .try_collect()
4541            .await
4542            .unwrap();
4543        let decided_view = chain.last().unwrap().leaf().view_number();
4544
4545        // Get the most recent state, for catchup.
4546
4547        let state = network.server.decided_state().await.unwrap();
4548        tracing::info!(?decided_view, ?state, "consensus state");
4549
4550        // Fully shut down the API servers.
4551        drop(network);
4552
4553        // Start up again, resuming from the last decided leaf.
4554        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4555
4556        let config = TestNetworkConfigBuilder::default()
4557            .api_config(SqlDataSource::options(
4558                &storage[0],
4559                Options::with_port(port),
4560            ))
4561            .persistences(persistence)
4562            .catchups(std::array::from_fn(|_| {
4563                // Catchup using node 0 as a peer. Node 0 was running the archival state service
4564                // before the restart, so it should be able to resume without catching up by loading
4565                // state from storage.
4566                StatePeers::<StaticVersion<0, 1>>::from_urls(
4567                    vec![format!("http://localhost:{port}").parse().unwrap()],
4568                    Default::default(),
4569                    Duration::from_secs(2),
4570                    &NoMetrics,
4571                )
4572            }))
4573            .network_config(TestConfigBuilder::default().build())
4574            .build();
4575        let _network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
4576        let client: Client<ClientErr, StaticVersion<0, 1>> =
4577            Client::new(format!("http://localhost:{port}").parse().unwrap());
4578        client.connect(None).await;
4579        tracing::info!(port, "server running");
4580
4581        // Make sure we can decide new blocks after the restart.
4582        tracing::info!("waiting for decide, height {height}");
4583        let new_leaf: LeafQueryData<SeqTypes> = client
4584            .socket(&format!("availability/stream/leaves/{height}"))
4585            .subscribe()
4586            .await
4587            .unwrap()
4588            .next()
4589            .await
4590            .unwrap()
4591            .unwrap();
4592        assert_eq!(new_leaf.height(), height as u64);
4593        assert_eq!(
4594            new_leaf.leaf().parent_commitment(),
4595            chain[height - 1].hash()
4596        );
4597
4598        // Ensure the new chain is consistent with the old chain.
4599        let new_chain: Vec<LeafQueryData<SeqTypes>> = client
4600            .socket("availability/stream/leaves/0")
4601            .subscribe()
4602            .await
4603            .unwrap()
4604            .take(height)
4605            .try_collect()
4606            .await
4607            .unwrap();
4608        assert_eq!(chain, new_chain);
4609    }
4610
4611    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4612    async fn test_fetch_config() {
4613        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4614        let url: Url = format!("http://localhost:{port}").parse().unwrap();
4615        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url.clone());
4616
4617        let options = Options::with_port(port).config(Default::default());
4618        let network_config = TestConfigBuilder::default().build();
4619        let config = TestNetworkConfigBuilder::default()
4620            .api_config(options)
4621            .network_config(network_config)
4622            .build();
4623        let network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
4624        client.connect(None).await;
4625
4626        // Fetch a network config from the API server. The first peer URL is bogus, to test the
4627        // failure/retry case.
4628        let peers = StatePeers::<StaticVersion<0, 1>>::from_urls(
4629            vec!["https://notarealnode.network".parse().unwrap(), url],
4630            Default::default(),
4631            Duration::from_secs(2),
4632            &NoMetrics,
4633        );
4634
4635        // Fetch the config from node 1, a different node than the one running the service.
4636        let validator =
4637            ValidatorConfig::generated_from_seed_indexed([0; 32], 1, U256::from(1), false);
4638        let config = peers.fetch_config(validator.clone()).await.unwrap();
4639
4640        // Check the node-specific information in the recovered config is correct.
4641        assert_eq!(config.node_index, 1);
4642
4643        // Check the public information is also correct (with respect to the node that actually
4644        // served the config, for public keys).
4645        pretty_assertions::assert_eq!(
4646            serde_json::to_value(PublicHotShotConfig::from(config.config)).unwrap(),
4647            serde_json::to_value(PublicHotShotConfig::from(
4648                network.cfg.hotshot_config().clone()
4649            ))
4650            .unwrap()
4651        );
4652    }
4653
4654    async fn run_hotshot_event_streaming_test(url_suffix: &str) {
4655        let query_service_port =
4656            reserve_tcp_port().expect("OS should have ephemeral ports available");
4657
4658        let url = format!("http://localhost:{query_service_port}{url_suffix}")
4659            .parse()
4660            .unwrap();
4661
4662        let client: Client<ClientErr, SequencerApiVersion> = Client::new(url);
4663
4664        let options = Options::with_port(query_service_port).hotshot_events(HotshotEvents);
4665
4666        let network_config = TestConfigBuilder::default().build();
4667        let config = TestNetworkConfigBuilder::default()
4668            .api_config(options)
4669            .network_config(network_config)
4670            .build();
4671        let _network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
4672
4673        let mut subscribed_events = client
4674            .socket("hotshot-events/events")
4675            .subscribe::<Event<SeqTypes>>()
4676            .await
4677            .unwrap();
4678
4679        let total_count = 5;
4680        // wait for these events to receive on client 1
4681        let mut receive_count = 0;
4682        loop {
4683            let event = subscribed_events.next().await.unwrap();
4684            tracing::info!("Received event in hotshot event streaming Client 1: {event:?}");
4685            receive_count += 1;
4686            if receive_count > total_count {
4687                tracing::info!("Client Received at least desired events, exiting loop");
4688                break;
4689            }
4690        }
4691        assert_eq!(receive_count, total_count + 1);
4692    }
4693
4694    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4695    async fn test_hotshot_event_streaming_v0() {
4696        run_hotshot_event_streaming_test("/v0").await;
4697    }
4698
4699    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4700    async fn test_hotshot_event_streaming_v1() {
4701        run_hotshot_event_streaming_test("/v1").await;
4702    }
4703
4704    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4705    async fn test_hotshot_event_streaming() {
4706        run_hotshot_event_streaming_test("").await;
4707    }
4708
4709    // TODO when `EPOCH_VERSION` becomes base version we can merge this
4710    // w/ above test.
4711    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4712    async fn test_hotshot_event_streaming_epoch_progression() {
4713        let epoch_height = 35;
4714        let wanted_epochs = 4;
4715
4716        let network_config = TestConfigBuilder::default()
4717            .epoch_height(epoch_height)
4718            .build();
4719
4720        let query_service_port =
4721            reserve_tcp_port().expect("OS should have ephemeral ports available");
4722
4723        let hotshot_url = format!("http://localhost:{query_service_port}")
4724            .parse()
4725            .unwrap();
4726
4727        let client: Client<ClientErr, SequencerApiVersion> = Client::new(hotshot_url);
4728        let options = Options::with_port(query_service_port).hotshot_events(HotshotEvents);
4729
4730        let config = TestNetworkConfigBuilder::default()
4731            .api_config(options)
4732            .network_config(network_config.clone())
4733            .pos_hook(
4734                DelegationConfig::VariableAmounts,
4735                Default::default(),
4736                POS_V3,
4737            )
4738            .await
4739            .expect("Pos Deployment")
4740            .build();
4741
4742        let _network = TestNetwork::new(config, POS_V3).await;
4743
4744        let mut subscribed_events = client
4745            .socket("hotshot-events/events")
4746            .subscribe::<Event<SeqTypes>>()
4747            .await
4748            .unwrap();
4749
4750        let wanted_views = epoch_height * wanted_epochs;
4751
4752        let mut views = HashSet::new();
4753        let mut epochs = HashSet::new();
4754        for _ in 0..=600 {
4755            let event = subscribed_events.next().await.unwrap();
4756            let event = event.unwrap();
4757            let view_number = event.view_number;
4758            views.insert(view_number.u64());
4759
4760            if let hotshot::types::EventType::Decide { committing_qc, .. } = event.event {
4761                assert!(committing_qc.epoch().is_some(), "epochs are live");
4762                assert!(committing_qc.block_number().is_some());
4763
4764                let epoch = committing_qc.epoch().unwrap().u64();
4765                epochs.insert(epoch);
4766
4767                tracing::debug!(
4768                    "Got decide: epoch: {:?}, block: {:?} ",
4769                    epoch,
4770                    committing_qc.block_number()
4771                );
4772
4773                let expected_epoch =
4774                    epoch_from_block_number(committing_qc.block_number().unwrap(), epoch_height);
4775                tracing::debug!("expected epoch: {expected_epoch}, qc epoch: {epoch}");
4776
4777                assert_eq!(expected_epoch, epoch);
4778            }
4779            if views.contains(&wanted_views) {
4780                tracing::info!("Client Received at least desired views, exiting loop");
4781                break;
4782            }
4783        }
4784
4785        // prevent false positive when we overflow the range
4786        assert!(views.contains(&wanted_views), "Views are not progressing");
4787        assert!(
4788            epochs.contains(&wanted_epochs),
4789            "Epochs are not progressing"
4790        );
4791    }
4792
4793    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4794    async fn test_pos_rewards_basic() -> anyhow::Result<()> {
4795        // Basic PoS rewards test:
4796        // - Sets up a single validator and a single delegator (the node itself).
4797        // - Sets the number of blocks in each epoch to 20.
4798        // - Rewards begin applying from block 41 (i.e., the start of the 3rd epoch).
4799        // - Since the validator is also the delegator, it receives the full reward.
4800        // - Verifies that the reward at block height 60 matches the expected amount.
4801        let epoch_height = 20;
4802
4803        let network_config = TestConfigBuilder::default()
4804            .epoch_height(epoch_height)
4805            .build();
4806
4807        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4808
4809        const NUM_NODES: usize = 1;
4810        // Initialize nodes.
4811        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
4812        let persistence: [_; NUM_NODES] = storage
4813            .iter()
4814            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
4815            .collect::<Vec<_>>()
4816            .try_into()
4817            .unwrap();
4818
4819        let config = TestNetworkConfigBuilder::with_num_nodes()
4820            .api_config(SqlDataSource::options(
4821                &storage[0],
4822                Options::with_port(api_port),
4823            ))
4824            .network_config(network_config.clone())
4825            .persistences(persistence.clone())
4826            .catchups(std::array::from_fn(|_| {
4827                StatePeers::<StaticVersion<0, 1>>::from_urls(
4828                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
4829                    Default::default(),
4830                    Duration::from_secs(2),
4831                    &NoMetrics,
4832                )
4833            }))
4834            .pos_hook(
4835                DelegationConfig::VariableAmounts,
4836                Default::default(),
4837                POS_V4,
4838            )
4839            .await
4840            .unwrap()
4841            .build();
4842
4843        let network = TestNetwork::new(config, POS_V4).await;
4844        let client: Client<ClientErr, SequencerApiVersion> =
4845            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
4846
4847        // first two epochs will be 1 and 2
4848        // rewards are distributed starting third epoch
4849        // third epoch starts from block 40 as epoch height is 20
4850        // wait for atleast 65 blocks
4851        let _blocks = client
4852            .socket("availability/stream/blocks/0")
4853            .subscribe::<BlockQueryData<SeqTypes>>()
4854            .await
4855            .unwrap()
4856            .take(65)
4857            .try_collect::<Vec<_>>()
4858            .await
4859            .unwrap();
4860
4861        let staking_priv_keys = network_config.staking_priv_keys();
4862        let account = staking_priv_keys[0].signer.clone();
4863        let address = account.address();
4864
4865        let block_height = 60;
4866
4867        let node_state = network.server.node_state();
4868        let membership = node_state.coordinator.membership();
4869        let expected_amount = U256::from(20)
4870            * (membership
4871                .epoch_block_reward(3.into())
4872                .expect("block reward is not None"))
4873            .0;
4874
4875        // get the validator address balance at block height 60
4876        let amount = client
4877            .get::<Option<RewardAmount>>(&format!(
4878                "reward-state/reward-balance/{block_height}/{address}"
4879            ))
4880            .send()
4881            .await
4882            .unwrap()
4883            .unwrap();
4884
4885        tracing::info!("amount={amount:?}");
4886
4887        assert_eq!(amount.0, expected_amount, "reward amount don't match");
4888
4889        Ok(())
4890    }
4891
4892    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4893    async fn test_cumulative_pos_rewards() -> anyhow::Result<()> {
4894        // This test registers 5 validators and multiple delegators for each validator.
4895        // One of the delegators is also a validator.
4896        // The test verifies that the cumulative reward at each block height equals
4897        // the total block reward, which is a constant.
4898
4899        let epoch_height = 20;
4900
4901        let network_config = TestConfigBuilder::default()
4902            .epoch_height(epoch_height)
4903            .build();
4904
4905        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4906
4907        const NUM_NODES: usize = 5;
4908        // Initialize nodes.
4909        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
4910        let persistence: [_; NUM_NODES] = storage
4911            .iter()
4912            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
4913            .collect::<Vec<_>>()
4914            .try_into()
4915            .unwrap();
4916
4917        let config = TestNetworkConfigBuilder::with_num_nodes()
4918            .api_config(SqlDataSource::options(
4919                &storage[0],
4920                Options::with_port(api_port),
4921            ))
4922            .network_config(network_config)
4923            .persistences(persistence.clone())
4924            .catchups(std::array::from_fn(|_| {
4925                StatePeers::<StaticVersion<0, 1>>::from_urls(
4926                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
4927                    Default::default(),
4928                    Duration::from_secs(2),
4929                    &NoMetrics,
4930                )
4931            }))
4932            .pos_hook(
4933                DelegationConfig::MultipleDelegators,
4934                Default::default(),
4935                POS_V4,
4936            )
4937            .await
4938            .unwrap()
4939            .build();
4940
4941        let network = TestNetwork::new(config, POS_V4).await;
4942        let node_state = network.server.node_state();
4943        let client: Client<ClientErr, SequencerApiVersion> =
4944            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
4945
4946        // wait for atleast 75 blocks
4947        let _blocks = client
4948            .socket("availability/stream/blocks/0")
4949            .subscribe::<BlockQueryData<SeqTypes>>()
4950            .await
4951            .unwrap()
4952            .take(75)
4953            .try_collect::<Vec<_>>()
4954            .await
4955            .unwrap();
4956
4957        // We are going to check cumulative blocks from block height 40 to 67
4958        // Basically epoch 3 and epoch 4 as epoch height is 20
4959        // get all the validators
4960        let validators = client
4961            .get::<AuthenticatedValidatorMap>("node/validators/3")
4962            .send()
4963            .await
4964            .expect("failed to get validator");
4965
4966        // insert all the address in a map
4967        // We will query the reward-balance at each block height for all the addresses
4968        // We don't know which validator was the leader because we don't have access to Membership
4969        let mut addresses = HashSet::new();
4970        for v in validators.values() {
4971            addresses.insert(v.account);
4972            addresses.extend(v.clone().delegators.keys().collect::<Vec<_>>());
4973        }
4974        // get all the validators
4975        let validators = client
4976            .get::<AuthenticatedValidatorMap>("node/validators/4")
4977            .send()
4978            .await
4979            .expect("failed to get validator");
4980        for v in validators.values() {
4981            addresses.insert(v.account);
4982            addresses.extend(v.clone().delegators.keys().collect::<Vec<_>>());
4983        }
4984
4985        let mut prev_cumulative_amount = U256::ZERO;
4986        // Check Cumulative rewards for epochs 3 (= block height 41 to 59) & 4 (= block height 60 to 67)
4987        for block in 41..=67 {
4988            let membership = node_state.coordinator.membership();
4989            let block_reward = membership
4990                .epoch_block_reward(epoch_from_block_number(block, epoch_height).into())
4991                .expect("block reward is not None");
4992
4993            let mut cumulative_amount = U256::ZERO;
4994            for address in addresses.clone() {
4995                let amount = client
4996                    .get::<Option<RewardAmount>>(&format!(
4997                        "reward-state/reward-balance/{block}/{address}"
4998                    ))
4999                    .send()
5000                    .await
5001                    .ok()
5002                    .flatten();
5003
5004                if let Some(amount) = amount {
5005                    tracing::info!("address={address}, amount={amount}");
5006                    cumulative_amount += amount.0;
5007                };
5008            }
5009
5010            // assert cumulative reward is equal to block reward
5011            assert_eq!(cumulative_amount - prev_cumulative_amount, block_reward.0);
5012            tracing::info!("cumulative_amount is correct for block={block}");
5013            prev_cumulative_amount = cumulative_amount;
5014        }
5015
5016        Ok(())
5017    }
5018
5019    #[test_log::test(tokio::test(flavor = "multi_thread"))]
5020    async fn test_stake_table_duplicate_events_from_contract() -> anyhow::Result<()> {
5021        // TODO(abdul): This test currently uses TestNetwork only for contract deployment and for L1 block number.
5022        // Once the stake table deployment logic is refactored and isolated, TestNetwork here will be unnecessary
5023
5024        let epoch_height = 20;
5025
5026        let network_config = TestConfigBuilder::default()
5027            .epoch_height(epoch_height)
5028            .build();
5029
5030        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
5031
5032        const NUM_NODES: usize = 5;
5033        // Initialize nodes.
5034        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
5035        let persistence: [_; NUM_NODES] = storage
5036            .iter()
5037            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
5038            .collect::<Vec<_>>()
5039            .try_into()
5040            .unwrap();
5041
5042        let l1_url = network_config.l1_url();
5043        let config = TestNetworkConfigBuilder::with_num_nodes()
5044            .api_config(SqlDataSource::options(
5045                &storage[0],
5046                Options::with_port(api_port),
5047            ))
5048            .network_config(network_config)
5049            .persistences(persistence.clone())
5050            .catchups(std::array::from_fn(|_| {
5051                StatePeers::<StaticVersion<0, 1>>::from_urls(
5052                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
5053                    Default::default(),
5054                    Duration::from_secs(2),
5055                    &NoMetrics,
5056                )
5057            }))
5058            .pos_hook(
5059                DelegationConfig::MultipleDelegators,
5060                Default::default(),
5061                POS_V3,
5062            )
5063            .await
5064            .unwrap()
5065            .build();
5066
5067        let network = TestNetwork::new(config, POS_V3).await;
5068
5069        let mut prev_st = None;
5070        let state = network.server.decided_state().await.unwrap();
5071        let chain_config = state.chain_config.resolve().expect("resolve chain config");
5072        let stake_table = chain_config.stake_table_contract.unwrap();
5073
5074        let l1_client = L1ClientOptions::default()
5075            .connect(vec![l1_url])
5076            .expect("failed to connect to l1");
5077
5078        let client: Client<ClientErr, SequencerApiVersion> =
5079            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
5080
5081        let mut headers = client
5082            .socket("availability/stream/headers/0")
5083            .subscribe::<Header>()
5084            .await
5085            .unwrap();
5086
5087        let mut target_bh = 0;
5088        while let Some(header) = headers.next().await {
5089            let header = header.unwrap();
5090            println!("got header with height {}", header.height());
5091            if header.height() == 0 {
5092                continue;
5093            }
5094            let l1_block = header.l1_finalized().expect("l1 block not found");
5095
5096            let sorted_events = Fetcher::fetch_events_from_contract(
5097                l1_client.clone(),
5098                stake_table,
5099                None,
5100                l1_block.number(),
5101            )
5102            .await?;
5103
5104            let mut sorted_dedup_removed = sorted_events.clone();
5105            sorted_dedup_removed.dedup();
5106
5107            assert_eq!(
5108                sorted_events.len(),
5109                sorted_dedup_removed.len(),
5110                "duplicates found"
5111            );
5112
5113            // This also checks if there is a duplicate registration
5114            let stake_table =
5115                validators_from_l1_events(sorted_events.into_iter().map(|(_, e)| e)).unwrap();
5116            if let Some(prev_st) = prev_st {
5117                assert_eq!(stake_table, prev_st);
5118            }
5119
5120            prev_st = Some(stake_table);
5121
5122            if target_bh == 100 {
5123                break;
5124            }
5125
5126            target_bh = header.height();
5127        }
5128
5129        Ok(())
5130    }
5131
5132    #[test_log::test(tokio::test(flavor = "multi_thread"))]
5133    async fn test_rewards_v4() -> anyhow::Result<()> {
5134        // This test verifies PoS reward distribution logic for multiple delegators per validator.
5135        //
5136        //  assertions:
5137        // - No rewards are distributed during the first 2 epochs.
5138        // - Rewards begin from epoch 3 onward.
5139        // - Delegator stake sums match the corresponding validator stake.
5140        // - Reward values match those returned by the reward state API.
5141        // - Commission calculations are within a small acceptable rounding tolerance.
5142        // - Ensure that the `total_reward_distributed` field in the block header matches the total block reward distributed
5143        const EPOCH_HEIGHT: u64 = 20;
5144
5145        let network_config = TestConfigBuilder::default()
5146            .epoch_height(EPOCH_HEIGHT)
5147            .build();
5148
5149        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
5150
5151        const NUM_NODES: usize = 5;
5152
5153        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
5154        let persistence: [_; NUM_NODES] = storage
5155            .iter()
5156            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
5157            .collect::<Vec<_>>()
5158            .try_into()
5159            .unwrap();
5160
5161        let config = TestNetworkConfigBuilder::with_num_nodes()
5162            .api_config(SqlDataSource::options(
5163                &storage[0],
5164                Options::with_port(api_port),
5165            ))
5166            .network_config(network_config)
5167            .persistences(persistence.clone())
5168            .catchups(std::array::from_fn(|_| {
5169                StatePeers::<StaticVersion<0, 1>>::from_urls(
5170                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
5171                    Default::default(),
5172                    Duration::from_secs(2),
5173                    &NoMetrics,
5174                )
5175            }))
5176            .pos_hook(
5177                DelegationConfig::MultipleDelegators,
5178                Default::default(),
5179                POS_V4,
5180            )
5181            .await
5182            .unwrap()
5183            .build();
5184
5185        let network = TestNetwork::new(config, POS_V4).await;
5186        let client: Client<ClientErr, SequencerApiVersion> =
5187            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
5188
5189        // Wait for the chain to progress beyond epoch 3 so rewards start being distributed.
5190        let mut events = network.peers[0].event_stream();
5191        while let Some(event) = events.next().await {
5192            if let CoordinatorEvent::LegacyEvent(Event {
5193                event: EventType::Decide { leaf_chain, .. },
5194                ..
5195            }) = event
5196            {
5197                let height = leaf_chain[0].leaf.height();
5198                tracing::info!("Node 0 decided at height: {height}");
5199                if height > EPOCH_HEIGHT * 3 {
5200                    break;
5201                }
5202            }
5203        }
5204
5205        // Verify that there are no validators for epoch # 1 and epoch # 2
5206        {
5207            client
5208                .get::<AuthenticatedValidatorMap>("node/validators/1")
5209                .send()
5210                .await
5211                .unwrap()
5212                .is_empty();
5213
5214            client
5215                .get::<AuthenticatedValidatorMap>("node/validators/2")
5216                .send()
5217                .await
5218                .unwrap()
5219                .is_empty();
5220        }
5221
5222        // Get the epoch # 3 validators
5223        let validators = client
5224            .get::<AuthenticatedValidatorMap>("node/validators/3")
5225            .send()
5226            .await
5227            .expect("validators");
5228
5229        assert!(!validators.is_empty());
5230
5231        // Collect addresses to track rewards for all participants.
5232        let mut addresses = HashSet::new();
5233        for v in validators.values() {
5234            addresses.insert(v.account);
5235            addresses.extend(v.clone().delegators.keys().collect::<Vec<_>>());
5236        }
5237
5238        let mut leaves = client
5239            .socket("availability/stream/leaves/0")
5240            .subscribe::<LeafQueryData<SeqTypes>>()
5241            .await
5242            .unwrap();
5243
5244        let node_state = network.server.node_state();
5245        let coordinator = node_state.coordinator;
5246
5247        let membership = coordinator.membership();
5248
5249        // Ensure rewards remain zero up for the first two epochs
5250        while let Some(leaf) = leaves.next().await {
5251            let leaf = leaf.unwrap();
5252            let header = leaf.header();
5253            assert_eq!(header.total_reward_distributed().unwrap().0, U256::ZERO);
5254
5255            let epoch_number =
5256                EpochNumber::new(epoch_from_block_number(leaf.height(), EPOCH_HEIGHT));
5257
5258            assert!(membership.epoch_block_reward(epoch_number).is_none());
5259
5260            let height = header.height();
5261            for address in addresses.clone() {
5262                let amount = client
5263                    .get::<Option<RewardAmount>>(&format!(
5264                        "reward-state-v2/reward-balance/{height}/{address}"
5265                    ))
5266                    .send()
5267                    .await
5268                    .ok()
5269                    .flatten();
5270                assert!(amount.is_none(), "amount is not none for block {height}")
5271            }
5272
5273            if leaf.height() == EPOCH_HEIGHT * 2 {
5274                break;
5275            }
5276        }
5277
5278        let mut rewards_map = HashMap::new();
5279        let mut total_distributed = U256::ZERO;
5280        let mut epoch_rewards = HashMap::<EpochNumber, U256>::new();
5281
5282        while let Some(leaf) = leaves.next().await {
5283            let leaf = leaf.unwrap();
5284
5285            let header = leaf.header();
5286            let distributed = header
5287                .total_reward_distributed()
5288                .expect("rewards distributed is none");
5289
5290            let block = leaf.height();
5291            tracing::info!("verify rewards for block={block:?}");
5292            let membership = coordinator.membership();
5293            let epoch_number =
5294                EpochNumber::new(epoch_from_block_number(leaf.height(), EPOCH_HEIGHT));
5295
5296            let snapshot = membership.snapshot(epoch_number).expect("snapshot");
5297            let block_reward = snapshot.epoch_block_reward().unwrap();
5298            let leader = snapshot.leader(leaf.leaf().view_number()).expect("leader");
5299            let leader_eth_address = snapshot
5300                .validator_config(&leader)
5301                .expect("validator config")
5302                .account;
5303
5304            let validators = client
5305                .get::<AuthenticatedValidatorMap>(&format!("node/validators/{epoch_number}"))
5306                .send()
5307                .await
5308                .expect("validators");
5309
5310            let leader_validator = validators
5311                .get(&leader_eth_address)
5312                .expect("leader not found");
5313
5314            let distributor =
5315                RewardDistributor::new(leader_validator.clone(), block_reward, distributed);
5316            // Verify that the sum of delegator stakes equals the validator's total stake.
5317            for validator in validators.values() {
5318                let delegator_stake_sum: U256 = validator.delegators.values().cloned().sum();
5319
5320                assert_eq!(delegator_stake_sum, validator.stake);
5321            }
5322
5323            let computed_rewards = distributor.compute_rewards().expect("reward computation");
5324
5325            // Validate that the leader's commission is within a 10 wei tolerance of the expected value.
5326            let total_reward = block_reward.0;
5327            let leader_commission_basis_points = U256::from(leader_validator.commission);
5328            let calculated_leader_commission_reward = leader_commission_basis_points
5329                .checked_mul(total_reward)
5330                .context("overflow")?
5331                .checked_div(U256::from(COMMISSION_BASIS_POINTS))
5332                .context("overflow")?;
5333
5334            assert!(
5335                computed_rewards.leader_commission().0 - calculated_leader_commission_reward
5336                    <= U256::from(10_u64)
5337            );
5338
5339            // Aggregate rewards by address (both delegator and leader).
5340            let leader_commission = *computed_rewards.leader_commission();
5341            for (address, amount) in computed_rewards.delegators().clone() {
5342                rewards_map
5343                    .entry(address)
5344                    .and_modify(|entry| *entry += amount)
5345                    .or_insert(amount);
5346            }
5347
5348            // add leader commission reward
5349            rewards_map
5350                .entry(leader_eth_address)
5351                .and_modify(|entry| *entry += leader_commission)
5352                .or_insert(leader_commission);
5353
5354            // assert that the reward matches to what is in the reward merkle tree
5355            for (address, calculated_amount) in rewards_map.iter() {
5356                let mut attempt = 0;
5357                let amount_from_api = loop {
5358                    let result = client
5359                        .get::<Option<RewardAmount>>(&format!(
5360                            "reward-state-v2/reward-balance/{block}/{address}"
5361                        ))
5362                        .send()
5363                        .await
5364                        .ok()
5365                        .flatten();
5366
5367                    if let Some(amount) = result {
5368                        break amount;
5369                    }
5370
5371                    attempt += 1;
5372                    if attempt >= 3 {
5373                        panic!(
5374                            "Failed to fetch reward amount for address {address} after 3 retries"
5375                        );
5376                    }
5377
5378                    sleep(Duration::from_secs(2)).await;
5379                };
5380
5381                assert_eq!(amount_from_api, *calculated_amount);
5382            }
5383
5384            // Confirm the header's total distributed field matches the cumulative expected amount.
5385            total_distributed += block_reward.0;
5386            assert_eq!(
5387                header.total_reward_distributed().unwrap().0,
5388                total_distributed
5389            );
5390
5391            // Block reward shouldn't change for the same epoch
5392            epoch_rewards
5393                .entry(epoch_number)
5394                .and_modify(|r| assert_eq!(*r, block_reward.0))
5395                .or_insert(block_reward.0);
5396
5397            // Stop the test after verifying 5 full epochs.
5398            if leaf.height() == EPOCH_HEIGHT * 5 {
5399                break;
5400            }
5401        }
5402
5403        Ok(())
5404    }
5405
5406    #[test_log::test(tokio::test(flavor = "multi_thread"))]
5407    async fn test_epoch_reward_distribution_basic() -> anyhow::Result<()> {
5408        const EPOCH_HEIGHT: u64 = 10;
5409        const NUM_NODES: usize = 5;
5410
5411        const V5: Upgrade = Upgrade::trivial(EPOCH_REWARD_VERSION);
5412
5413        let network_config = TestConfigBuilder::default()
5414            .epoch_height(EPOCH_HEIGHT)
5415            .build();
5416
5417        let api_port = reserve_tcp_port().expect("No ports free for query service");
5418
5419        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
5420        let persistence: [_; NUM_NODES] = storage
5421            .iter()
5422            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
5423            .collect::<Vec<_>>()
5424            .try_into()
5425            .unwrap();
5426
5427        let config = TestNetworkConfigBuilder::with_num_nodes()
5428            .api_config(SqlDataSource::options(
5429                &storage[0],
5430                Options::with_port(api_port),
5431            ))
5432            .network_config(network_config)
5433            .persistences(persistence.clone())
5434            .catchups(std::array::from_fn(|_| {
5435                StatePeers::<StaticVersion<0, 1>>::from_urls(
5436                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
5437                    Default::default(),
5438                    Duration::from_secs(2),
5439                    &NoMetrics,
5440                )
5441            }))
5442            .pos_hook(DelegationConfig::MultipleDelegators, Default::default(), V5)
5443            .await
5444            .unwrap()
5445            .build();
5446
5447        let _network = TestNetwork::new(config, V5).await;
5448        let client: Client<ClientErr, SequencerApiVersion> =
5449            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
5450
5451        // Wait for chain to reach epoch 5
5452        let height_client: Client<ClientErr, StaticVersion<0, 1>> =
5453            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
5454        wait_until_block_height(&height_client, "node/block-height", EPOCH_HEIGHT * 5).await;
5455
5456        let mut leaves = client
5457            .socket("availability/stream/leaves/0")
5458            .subscribe::<LeafQueryData<SeqTypes>>()
5459            .await
5460            .unwrap();
5461
5462        // Epochs 1-3: verify no rewards
5463        while let Some(leaf) = leaves.next().await {
5464            let leaf = leaf.unwrap();
5465            let header = leaf.header();
5466            let height = header.height();
5467
5468            let total_distributed = header.total_reward_distributed().unwrap();
5469            assert_eq!(
5470                total_distributed.0,
5471                U256::ZERO,
5472                "epochs 1-3 should have no rewards, height={height}"
5473            );
5474
5475            if height == EPOCH_HEIGHT * 3 {
5476                break;
5477            }
5478        }
5479
5480        while let Some(leaf) = leaves.next().await {
5481            let leaf = leaf.unwrap();
5482            let header = leaf.header();
5483            let height = header.height();
5484
5485            if height == EPOCH_HEIGHT * 4 {
5486                let total_distributed = header.total_reward_distributed().unwrap();
5487                assert!(total_distributed.0 > U256::ZERO,);
5488                break;
5489            }
5490        }
5491
5492        while let Some(leaf) = leaves.next().await {
5493            let leaf = leaf.unwrap();
5494            let header = leaf.header();
5495            let height = header.height();
5496
5497            if height == EPOCH_HEIGHT * 5 {
5498                let total_distributed = header.total_reward_distributed().unwrap();
5499                assert!(total_distributed.0 > U256::ZERO,);
5500                break;
5501            }
5502        }
5503
5504        Ok(())
5505    }
5506
5507    /// Run a `TestNetwork` based directly on the new protocol version (V0_6,
5508    /// no upgrade/cutover) and verify it produces blocks from genesis.
5509    #[test_log::test(tokio::test(flavor = "multi_thread"))]
5510    async fn test_new_protocol_produces_blocks() -> anyhow::Result<()> {
5511        const EPOCH_HEIGHT: u64 = 100;
5512        const NUM_NODES: usize = 5;
5513        const TARGET_BLOCK_HEIGHT: u64 = 100;
5514
5515        const NEW_PROTOCOL: Upgrade = Upgrade::trivial(NEW_PROTOCOL_VERSION);
5516
5517        let network_config = TestConfigBuilder::default()
5518            .epoch_height(EPOCH_HEIGHT)
5519            .epoch_start_block(0)
5520            .build();
5521
5522        let api_port = reserve_tcp_port().expect("No ports free for query service");
5523
5524        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
5525        let persistence: [_; NUM_NODES] = storage
5526            .iter()
5527            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
5528            .collect::<Vec<_>>()
5529            .try_into()
5530            .unwrap();
5531
5532        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
5533            .api_config(SqlDataSource::options(
5534                &storage[0],
5535                Options::with_port(api_port),
5536            ))
5537            .network_config(network_config)
5538            .persistences(persistence)
5539            .catchups(std::array::from_fn(|_| {
5540                StatePeers::<SequencerApiVersion>::from_urls(
5541                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
5542                    Default::default(),
5543                    Duration::from_secs(2),
5544                    &NoMetrics,
5545                )
5546            }))
5547            .pos_hook(
5548                DelegationConfig::MultipleDelegators,
5549                StakeTableContractVersion::V3,
5550                NEW_PROTOCOL,
5551            )
5552            .await
5553            .unwrap()
5554            .build();
5555
5556        let _network = TestNetwork::new(config, NEW_PROTOCOL).await;
5557
5558        let client: Client<ClientErr, SequencerApiVersion> =
5559            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
5560        client.connect(Some(Duration::from_secs(30))).await;
5561
5562        let mut leaves = client
5563            .socket("availability/stream/leaves/0")
5564            .subscribe::<LeafQueryData<SeqTypes>>()
5565            .await
5566            .expect("subscribe to leaf stream");
5567
5568        let mut height = 0;
5569        while let Some(leaf) = leaves.next().await {
5570            let leaf = leaf.expect("leaf stream yielded an error");
5571            let header = leaf.header();
5572            height = header.height();
5573
5574            if height > 0 {
5575                assert_eq!(
5576                    header.version(),
5577                    NEW_PROTOCOL_VERSION,
5578                    "block {height} should be produced under the new protocol version",
5579                );
5580            }
5581
5582            if height >= TARGET_BLOCK_HEIGHT {
5583                break;
5584            }
5585        }
5586
5587        assert!(
5588            height >= TARGET_BLOCK_HEIGHT,
5589            "expected at least {TARGET_BLOCK_HEIGHT} blocks, got {height} (leaf stream ended \
5590             early)",
5591        );
5592
5593        Ok(())
5594    }
5595
5596    /// Run entirely without the legacy consensus stack: with base version
5597    /// `NEW_PROTOCOL_VERSION` it is torn down at startup, and the explicit
5598    /// mid-run `shut_down_legacy` calls below (what the decide-count trigger
5599    /// in `handle_events` does after `LEGACY_SHUTDOWN_DECIDE_COUNT` decides
5600    /// on an upgraded network) must be harmless to repeat. The network has
5601    /// to keep deciding across epoch boundaries: DRB computations on the
5602    /// shared membership coordinator must survive the teardown.
5603    #[test_log::test(tokio::test(flavor = "multi_thread"))]
5604    async fn test_new_protocol_survives_legacy_shutdown() -> anyhow::Result<()> {
5605        const EPOCH_HEIGHT: u64 = 20;
5606        const NUM_NODES: usize = 5;
5607        const SHUTDOWN_HEIGHT: u64 = 10;
5608        const TARGET_BLOCK_HEIGHT: u64 = 50;
5609
5610        const NEW_PROTOCOL: Upgrade = Upgrade::trivial(NEW_PROTOCOL_VERSION);
5611
5612        let network_config = TestConfigBuilder::default()
5613            .epoch_height(EPOCH_HEIGHT)
5614            .epoch_start_block(0)
5615            .build();
5616
5617        let api_port = reserve_tcp_port().expect("No ports free for query service");
5618
5619        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
5620        let persistence: [_; NUM_NODES] = storage
5621            .iter()
5622            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
5623            .collect::<Vec<_>>()
5624            .try_into()
5625            .unwrap();
5626
5627        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
5628            .api_config(SqlDataSource::options(
5629                &storage[0],
5630                Options::with_port(api_port),
5631            ))
5632            .network_config(network_config)
5633            .persistences(persistence)
5634            .catchups(std::array::from_fn(|_| {
5635                StatePeers::<SequencerApiVersion>::from_urls(
5636                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
5637                    Default::default(),
5638                    Duration::from_secs(2),
5639                    &NoMetrics,
5640                )
5641            }))
5642            .pos_hook(
5643                DelegationConfig::MultipleDelegators,
5644                StakeTableContractVersion::V3,
5645                NEW_PROTOCOL,
5646            )
5647            .await
5648            .unwrap()
5649            .build();
5650
5651        let network = TestNetwork::new(config, NEW_PROTOCOL).await;
5652
5653        let client: Client<ClientErr, SequencerApiVersion> =
5654            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
5655        client.connect(Some(Duration::from_secs(30))).await;
5656
5657        let mut leaves = client
5658            .socket("availability/stream/leaves/0")
5659            .subscribe::<LeafQueryData<SeqTypes>>()
5660            .await
5661            .expect("subscribe to leaf stream");
5662
5663        // Let the new protocol decide a few blocks first.
5664        let mut height = 0;
5665        while height < SHUTDOWN_HEIGHT {
5666            let leaf = leaves
5667                .next()
5668                .await
5669                .expect("leaf stream ended early")
5670                .expect("leaf stream yielded an error");
5671            height = leaf.header().height();
5672        }
5673
5674        // Tear down the legacy stack on every node.
5675        network.server.consensus_handle().shut_down_legacy().await;
5676        for peer in &network.peers {
5677            peer.consensus_handle().shut_down_legacy().await;
5678        }
5679
5680        // The chain must keep growing across the epoch boundaries at 20 and
5681        // 40 purely on the new protocol.
5682        while height < TARGET_BLOCK_HEIGHT {
5683            let leaf = leaves
5684                .next()
5685                .await
5686                .expect("leaf stream ended early")
5687                .expect("leaf stream yielded an error");
5688            height = leaf.header().height();
5689        }
5690
5691        Ok(())
5692    }
5693
5694    /// Full-application epoch-boundary committee change: a validator sends a
5695    /// real `deregisterValidator` transaction to the StakeTable contract on
5696    /// L1 mid-run, drops out of the consensus committee at the epoch boundary
5697    /// where the exit activates, and the reduced committee keeps deciding.
5698    ///
5699    /// Runs genesis at `NEW_PROTOCOL_VERSION` (StakeTable V3), so this covers
5700    /// the whole pipeline the HotShot-level tests in
5701    /// `hotshot-new-protocol/src/tests/stake_table_changes.rs` mock out:
5702    /// L1 event fetching, `select_active_validator_set`, and epoch-root-driven
5703    /// membership updates.
5704    #[test_log::test(tokio::test(flavor = "multi_thread"))]
5705    async fn test_new_protocol_validator_exit_at_epoch_boundary() -> anyhow::Result<()> {
5706        const NUM_NODES: usize = 5;
5707        const EPOCH_HEIGHT: u64 = 10;
5708        const NEW_PROTOCOL: Upgrade = Upgrade::trivial(NEW_PROTOCOL_VERSION);
5709        /// How many epochs after the exit transaction we allow for the event
5710        /// to finalize on L1 and reach a stake table snapshot before failing.
5711        const MAX_ACTIVATION_EPOCHS: u64 = 10;
5712
5713        let network_config = TestConfigBuilder::default()
5714            .epoch_height(EPOCH_HEIGHT)
5715            .epoch_start_block(0)
5716            .build();
5717
5718        let api_port = reserve_tcp_port().expect("No ports free for query service");
5719
5720        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
5721        let persistence: [_; NUM_NODES] = storage
5722            .iter()
5723            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
5724            .collect::<Vec<_>>()
5725            .try_into()
5726            .unwrap();
5727
5728        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
5729            .api_config(SqlDataSource::options(
5730                &storage[0],
5731                Options::with_port(api_port),
5732            ))
5733            .network_config(network_config.clone())
5734            .persistences(persistence)
5735            .catchups(std::array::from_fn(|_| {
5736                StatePeers::<SequencerApiVersion>::from_urls(
5737                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
5738                    Default::default(),
5739                    Duration::from_secs(2),
5740                    &NoMetrics,
5741                )
5742            }))
5743            .pos_hook(
5744                DelegationConfig::MultipleDelegators,
5745                StakeTableContractVersion::V3,
5746                NEW_PROTOCOL,
5747            )
5748            .await
5749            .unwrap()
5750            .build();
5751
5752        let network = TestNetwork::new(config, NEW_PROTOCOL).await;
5753        let st_addr = network
5754            .contracts
5755            .as_ref()
5756            .unwrap()
5757            .address(Contract::StakeTableProxy)
5758            .unwrap();
5759
5760        // Exit the last node's validator. The node keeps running (matching
5761        // the HotShot-level leave test), but must disappear from the
5762        // committee. Node 0 serves the query API, so it stays a member.
5763        let (exiting, exiting_provider) = network_config
5764            .validator_providers()
5765            .pop()
5766            .expect("at least one validator");
5767
5768        let client: Client<ClientErr, SequencerApiVersion> =
5769            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
5770        client.connect(Some(Duration::from_secs(30))).await;
5771
5772        // Let the network settle into normal epoch operation before exiting.
5773        let mut events = network.peers[0].event_stream();
5774        wait_for_epochs(&mut events, EPOCH_HEIGHT, 3).await;
5775        let exit_epoch = network.peers[0]
5776            .decided_leaf()
5777            .await
5778            .epoch(EPOCH_HEIGHT)
5779            .unwrap()
5780            .u64();
5781
5782        let validators = client
5783            .get::<AuthenticatedValidatorMap>(&format!("node/validators/{exit_epoch}"))
5784            .send()
5785            .await
5786            .expect("validators for the exit epoch");
5787        assert!(
5788            validators.contains_key(&exiting),
5789            "exiting validator should be in the committee before its exit activates"
5790        );
5791        assert_eq!(validators.len(), NUM_NODES);
5792
5793        let receipt = StakingTransaction::DeregisterValidator {
5794            stake_table: st_addr,
5795        }
5796        .send(&exiting_provider)
5797        .await?
5798        .get_receipt()
5799        .await?;
5800        assert!(receipt.status(), "deregistration transaction reverted");
5801
5802        // The exit activates at the first epoch whose stake table snapshot is
5803        // taken after the event finalizes on L1; scan forward until it does.
5804        let mut activation_epoch = None;
5805        for epoch in exit_epoch + 1..=exit_epoch + MAX_ACTIVATION_EPOCHS {
5806            wait_for_epochs(&mut events, EPOCH_HEIGHT, epoch).await;
5807            let validators = client
5808                .get::<AuthenticatedValidatorMap>(&format!("node/validators/{epoch}"))
5809                .send()
5810                .await
5811                .expect("validators for a decided epoch");
5812            if !validators.contains_key(&exiting) {
5813                assert_eq!(
5814                    validators.len(),
5815                    NUM_NODES - 1,
5816                    "only the exited validator should have left the committee"
5817                );
5818                activation_epoch = Some(epoch);
5819                break;
5820            }
5821            assert_eq!(validators.len(), NUM_NODES);
5822        }
5823        let activation_epoch = activation_epoch.unwrap_or_else(|| {
5824            panic!("validator still in the committee {MAX_ACTIVATION_EPOCHS} epochs after exiting")
5825        });
5826
5827        // The reduced committee must keep deciding across further epoch
5828        // boundaries.
5829        wait_for_epochs(&mut events, EPOCH_HEIGHT, activation_epoch + 2).await;
5830
5831        Ok(())
5832    }
5833
5834    /// A query node whose database is wiped has to rebuild itself from its peer
5835    /// and rejoin consensus.
5836    ///
5837    /// Six nodes run the new protocol from genesis. Nodes 0 and 1 both serve the
5838    /// query API and are peered at each other so either can backfill from the
5839    /// other; nodes 2 through 5 are plain validators. Once the network decides
5840    /// past epoch seven, node 1 is taken down and started again on an empty
5841    /// database: no consensus state, no archive, no merklized state. To recover
5842    /// it has to bootstrap its stake-table window, resync consensus, backfill
5843    /// the chain it lost from node 0 (including the new protocol's cert2
5844    /// finality certificates), and resume proposing.
5845    #[test_log::test(tokio::test(flavor = "multi_thread"))]
5846    async fn test_new_protocol_query_node_restart_with_fresh_storage() -> anyhow::Result<()> {
5847        const NUM_NODES: usize = 6;
5848        const EPOCH_HEIGHT: u64 = 10;
5849        const EPOCHS_BEFORE_RESTART: u64 = 7;
5850        const NEW_PROTOCOL: Upgrade = Upgrade::trivial(NEW_PROTOCOL_VERSION);
5851        /// Bound on each stage of the restarted node's recovery.
5852        const RECOVERY_TIMEOUT: Duration = Duration::from_secs(240);
5853
5854        let network_config = TestConfigBuilder::default()
5855            .epoch_height(EPOCH_HEIGHT)
5856            .epoch_start_block(0)
5857            .build();
5858
5859        let api_port = reserve_tcp_port().expect("No ports free for query service");
5860        let query_port = reserve_tcp_port().expect("No ports free for query service");
5861        let api_url: Url = format!("http://localhost:{api_port}").parse()?;
5862        let query_url: Url = format!("http://localhost:{query_port}").parse()?;
5863
5864        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
5865        let persistence: [_; NUM_NODES] = storage
5866            .iter()
5867            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
5868            .collect::<Vec<_>>()
5869            .try_into()
5870            .unwrap();
5871
5872        // Both query nodes need the catchup and light client modules: state
5873        // catchup is served over the former, and the query service validates
5874        // leaves fetched from a peer with light client proofs served over the
5875        // latter. Every node can catch up from either query node.
5876        let query_urls = vec![api_url.clone(), query_url.clone()];
5877        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
5878            .api_config(
5879                Options::with_port(api_port)
5880                    .catchup(Default::default())
5881                    .light_client(Default::default())
5882                    .query_sql(
5883                        Query {
5884                            peers: vec![query_url.clone()],
5885                            ..Default::default()
5886                        },
5887                        tmp_options(&storage[0]),
5888                    ),
5889            )
5890            .network_config(network_config)
5891            .persistences(persistence.clone())
5892            .catchups(std::array::from_fn(|_| {
5893                StatePeers::<SequencerApiVersion>::from_urls(
5894                    query_urls.clone(),
5895                    Default::default(),
5896                    Duration::from_secs(2),
5897                    &NoMetrics,
5898                )
5899            }))
5900            .pos_hook(
5901                DelegationConfig::MultipleDelegators,
5902                StakeTableContractVersion::V3,
5903                NEW_PROTOCOL,
5904            )
5905            .await?
5906            .build();
5907
5908        let genesis_state = config.states()[0].clone();
5909        let mut network = TestNetwork::new(config, NEW_PROTOCOL).await;
5910
5911        // `TestNetwork` only gives node 0 an API, so node 1 has to be served
5912        // separately to become the second query node. Its keys are already in the
5913        // config, so it rejoins under the same index.
5914        network.peers[0].shut_down().await;
5915        network.peers.remove(0);
5916
5917        let cfg = network.cfg.clone();
5918        let peer_url = api_url.clone();
5919        let start_query_node = move |db: persistence::sql::Options| {
5920            let cfg = cfg.clone();
5921            let genesis_state = genesis_state.clone();
5922            let peer_url = peer_url.clone();
5923            async move {
5924                let opt = Options::with_port(query_port)
5925                    .catchup(Default::default())
5926                    .light_client(Default::default())
5927                    .query_sql(
5928                        Query {
5929                            peers: vec![peer_url.clone()],
5930                            ..Default::default()
5931                        },
5932                        db.clone(),
5933                    );
5934                let ctx = opt
5935                    .serve(move |metrics, consumer, storage| {
5936                        async move {
5937                            Ok(cfg
5938                                .init_node(
5939                                    1,
5940                                    genesis_state,
5941                                    db,
5942                                    Some(StatePeers::<SequencerApiVersion>::from_urls(
5943                                        vec![peer_url],
5944                                        Default::default(),
5945                                        Duration::from_secs(2),
5946                                        &NoMetrics,
5947                                    )),
5948                                    storage,
5949                                    &*metrics,
5950                                    test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
5951                                    consumer,
5952                                    NEW_PROTOCOL,
5953                                    Default::default(),
5954                                )
5955                                .await)
5956                        }
5957                        .boxed()
5958                    })
5959                    .await
5960                    .expect("second query node should start");
5961                ctx.start_consensus().await;
5962                ctx
5963            }
5964        };
5965
5966        let mut query_node = start_query_node(tmp_options(&storage[1])).await;
5967
5968        let api_client: Client<ClientErr, SequencerApiVersion> = Client::new(api_url);
5969        let query_client: Client<ClientErr, SequencerApiVersion> = Client::new(query_url);
5970        assert!(
5971            api_client.connect(Some(Duration::from_secs(60))).await,
5972            "node 0 query API did not come up"
5973        );
5974        assert!(
5975            query_client.connect(Some(Duration::from_secs(60))).await,
5976            "node 1 query API did not come up"
5977        );
5978
5979        // Run the network for seven epochs, watching a validator that stays up
5980        // for the whole test.
5981        let mut events = network.peers[0].event_stream();
5982        wait_for_epochs(&mut events, EPOCH_HEIGHT, EPOCHS_BEFORE_RESTART).await;
5983
5984        // The node about to be wiped must be healthy first, otherwise the test
5985        // would prove nothing about recovering from a wipe.
5986        let height_before_restart = query_node.decided_leaf().await.height();
5987        timeout(
5988            RECOVERY_TIMEOUT,
5989            wait_until_block_height(
5990                &query_client,
5991                "block-state/block-height",
5992                height_before_restart,
5993            ),
5994        )
5995        .await
5996        .context("second query node was not caught up before the restart")?;
5997
5998        // Pick a height the new protocol finalized with a cert2 to check the
5999        // restarted node backfills those too. A cert2 is only stored at the
6000        // height it finalizes, so scan back from the tip for one.
6001        let mut finalized_height = None;
6002        for height in (1..height_before_restart).rev() {
6003            if api_client
6004                .get::<espresso_types::Certificate2<SeqTypes>>(&format!(
6005                    "availability/cert2/{height}"
6006                ))
6007                .send()
6008                .await
6009                .is_ok()
6010            {
6011                finalized_height = Some(height);
6012                break;
6013            }
6014        }
6015        let finalized_height =
6016            finalized_height.context("no cert2 stored on a new protocol chain")?;
6017
6018        tracing::info!(
6019            height_before_restart,
6020            finalized_height,
6021            "restarting the second query node with fresh storage"
6022        );
6023        query_node.shut_down().await;
6024        // The node has to come back on the same ports: node 0 dials the query
6025        // API at the peer URL baked in at network construction, and cliquenet
6026        // peers dial the address registered in the stake table. `shut_down`
6027        // aborts the server tasks without waiting for them to finish, so poll
6028        // until the old listeners are actually gone before rebinding.
6029        let cliquenet_port = network.cfg.known_nodes_with_stake()[1]
6030            .connect_info
6031            .as_ref()
6032            .expect("node 1 registered cliquenet connect info")
6033            .p2p_addr
6034            .port();
6035        timeout(RECOVERY_TIMEOUT, async {
6036            for port in [query_port, cliquenet_port] {
6037                while std::net::TcpListener::bind(("127.0.0.1", port)).is_err() {
6038                    sleep(Duration::from_millis(100)).await;
6039                }
6040            }
6041        })
6042        .await
6043        .context("shut-down node did not release its ports")?;
6044
6045        let fresh_storage = SqlDataSource::create_storage().await;
6046        let query_node = start_query_node(tmp_options(&fresh_storage)).await;
6047        assert!(
6048            query_client.connect(Some(Duration::from_secs(60))).await,
6049            "restarted query API did not come up"
6050        );
6051
6052        // First it has to get back to the height it lost, in both the archive and
6053        // the merklized state derived from it.
6054        timeout(
6055            RECOVERY_TIMEOUT,
6056            wait_until_block_height(&query_client, "status/block-height", height_before_restart),
6057        )
6058        .await
6059        .context("restarted node did not rebuild its archive")?;
6060        timeout(
6061            RECOVERY_TIMEOUT,
6062            wait_until_block_height(
6063                &query_client,
6064                "block-state/block-height",
6065                height_before_restart,
6066            ),
6067        )
6068        .await
6069        .context("restarted node did not rebuild its merklized state")?;
6070
6071        // Reaching the tip only proves it is following consensus again, so stream
6072        // the whole range it lost from both nodes: the restarted node's copy can
6073        // only have come from node 0, and it has to be the chain the network
6074        // actually decided. Streaming also forces the restarted node to backfill
6075        // every leaf in the range, since the stream endpoint fetches on demand.
6076        let wiped_range = (height_before_restart - 1) as usize;
6077        let stream_leaves = |client: Client<ClientErr, SequencerApiVersion>, who: &'static str| async move {
6078            let leaves: Vec<LeafQueryData<SeqTypes>> = client
6079                .socket("availability/stream/leaves/1")
6080                .subscribe()
6081                .await
6082                .with_context(|| format!("subscribing to {who}'s leaf stream"))?
6083                .take(wiped_range)
6084                .try_collect()
6085                .await
6086                .with_context(|| format!("{who}'s leaf stream errored"))?;
6087            anyhow::Ok(leaves)
6088        };
6089        // Node 0's stream returns immediately, so the shared timeout is in
6090        // practice a bound on the restarted node's backfill.
6091        let (ours, theirs) = timeout(
6092            RECOVERY_TIMEOUT,
6093            future::try_join(
6094                stream_leaves(query_client.clone(), "the restarted node"),
6095                stream_leaves(api_client.clone(), "node 0"),
6096            ),
6097        )
6098        .await
6099        .context("streaming the wiped range stalled")??;
6100        assert_eq!(
6101            ours.len(),
6102            wiped_range,
6103            "restarted node's leaf stream ended early"
6104        );
6105        assert_eq!(
6106            theirs.len(),
6107            wiped_range,
6108            "node 0's leaf stream ended early"
6109        );
6110        for (ours, theirs) in ours.iter().zip(&theirs) {
6111            assert_eq!(
6112                ours.hash(),
6113                theirs.hash(),
6114                "restarted node's leaf at height {} diverges from node 0",
6115                ours.height(),
6116            );
6117            assert_eq!(
6118                ours.header().version(),
6119                NEW_PROTOCOL_VERSION,
6120                "block {} should have been produced under the new protocol",
6121                ours.height(),
6122            );
6123        }
6124
6125        let cert2 = timeout(RECOVERY_TIMEOUT, async {
6126            loop {
6127                match query_client
6128                    .get::<espresso_types::Certificate2<SeqTypes>>(&format!(
6129                        "availability/cert2/{finalized_height}"
6130                    ))
6131                    .send()
6132                    .await
6133                {
6134                    Ok(cert2) => break cert2,
6135                    Err(err) => {
6136                        tracing::info!(finalized_height, %err, "cert2 not backfilled yet")
6137                    },
6138                }
6139                sleep(Duration::from_secs(2)).await;
6140            }
6141        })
6142        .await
6143        .context("restarted node did not backfill the cert2")?;
6144        assert_eq!(cert2.data.block_number, finalized_height);
6145
6146        // Tracking decides is not enough to call the node a participant: wait for
6147        // a block it proposed itself to be decided after the restart.
6148        let node_1_key = network.cfg.known_nodes_with_stake()[1]
6149            .stake_table_entry
6150            .stake_key;
6151        let coordinator = query_node.node_state().coordinator;
6152        let mut events = query_node.event_stream();
6153        let proposed = timeout(RECOVERY_TIMEOUT, async {
6154            while let Some(event) = events.next().await {
6155                let leaf_infos: &[LeafInfo<SeqTypes>] = match &event {
6156                    CoordinatorEvent::LegacyEvent(Event {
6157                        event: EventType::Decide { leaf_chain, .. },
6158                        ..
6159                    }) => leaf_chain,
6160                    CoordinatorEvent::NewDecide { leaf_infos, .. } => leaf_infos,
6161                    _ => continue,
6162                };
6163                for LeafInfo { leaf, .. } in leaf_infos {
6164                    if leaf.height() <= height_before_restart {
6165                        continue;
6166                    }
6167                    let membership =
6168                        match coordinator.membership_for_epoch(leaf.epoch(EPOCH_HEIGHT)) {
6169                            Ok(membership) => membership,
6170                            Err(err) => {
6171                                tracing::warn!(
6172                                    height = leaf.height(),
6173                                    %err,
6174                                    "no membership for epoch",
6175                                );
6176                                continue;
6177                            },
6178                        };
6179                    match membership.leader(leaf.view_number()) {
6180                        Ok(leader) if leader == node_1_key => return Some(leaf.height()),
6181                        Ok(_) => {},
6182                        Err(err) => {
6183                            tracing::warn!(view = ?leaf.view_number(), %err, "leader unresolved");
6184                        },
6185                    }
6186                }
6187            }
6188            None
6189        })
6190        .await
6191        .context("restarted node's event stream stalled")?;
6192        let proposed = proposed.context("restarted node's event stream ended")?;
6193        tracing::info!(proposed, "restarted node proposed a decided block");
6194
6195        Ok(())
6196    }
6197
6198    #[test_log::test(tokio::test(flavor = "multi_thread"))]
6199    async fn test_epoch_reward_total_distributed_rewards() -> anyhow::Result<()> {
6200        // Epochs 1-3: No rewards distributed (total_reward_distributed = 0)
6201        // Epoch 4: Rewards only distributed in the LAST block
6202        // Epoch 5: All blocks before last have same total as epoch 4 last block,
6203        //          last block has higher total because of new distribution
6204        const EPOCH_HEIGHT: u64 = 10;
6205        const NUM_NODES: usize = 5;
6206
6207        const V5: Upgrade = Upgrade::trivial(EPOCH_REWARD_VERSION);
6208
6209        let network_config = TestConfigBuilder::default()
6210            .epoch_height(EPOCH_HEIGHT)
6211            .build();
6212
6213        let api_port = reserve_tcp_port().expect("No ports free for query service");
6214
6215        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
6216        let persistence: [_; NUM_NODES] = storage
6217            .iter()
6218            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
6219            .collect::<Vec<_>>()
6220            .try_into()
6221            .unwrap();
6222
6223        let config = TestNetworkConfigBuilder::with_num_nodes()
6224            .api_config(SqlDataSource::options(
6225                &storage[0],
6226                Options::with_port(api_port),
6227            ))
6228            .network_config(network_config)
6229            .persistences(persistence.clone())
6230            .catchups(std::array::from_fn(|_| {
6231                StatePeers::<StaticVersion<0, 1>>::from_urls(
6232                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
6233                    Default::default(),
6234                    Duration::from_secs(2),
6235                    &NoMetrics,
6236                )
6237            }))
6238            .pos_hook(DelegationConfig::MultipleDelegators, Default::default(), V5)
6239            .await
6240            .unwrap()
6241            .build();
6242
6243        let _network = TestNetwork::new(config, V5).await;
6244        let client: Client<ClientErr, SequencerApiVersion> =
6245            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
6246
6247        let height_client: Client<ClientErr, StaticVersion<0, 1>> =
6248            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
6249        wait_until_block_height(&height_client, "node/block-height", EPOCH_HEIGHT * 5).await;
6250
6251        let mut leaves = client
6252            .socket("availability/stream/leaves/0")
6253            .subscribe::<LeafQueryData<SeqTypes>>()
6254            .await
6255            .unwrap();
6256
6257        while let Some(leaf) = leaves.next().await {
6258            let leaf = leaf.unwrap();
6259            let header = leaf.header();
6260            let height = header.height();
6261
6262            let total_distributed = header.total_reward_distributed().unwrap();
6263            assert_eq!(total_distributed.0, U256::ZERO,);
6264
6265            if height == EPOCH_HEIGHT * 3 {
6266                break;
6267            }
6268        }
6269
6270        while let Some(leaf) = leaves.next().await {
6271            let leaf = leaf.unwrap();
6272            let header = leaf.header();
6273            let height = header.height();
6274
6275            let total_distributed = header.total_reward_distributed().unwrap();
6276
6277            if height < EPOCH_HEIGHT * 4 {
6278                assert_eq!(total_distributed.0, U256::ZERO,);
6279            } else {
6280                assert!(total_distributed.0 > U256::ZERO,);
6281                break;
6282            }
6283        }
6284
6285        let epoch4_last_reward = {
6286            let header = client
6287                .get::<Header>(&format!("availability/header/{}", EPOCH_HEIGHT * 4))
6288                .send()
6289                .await
6290                .unwrap();
6291            header.total_reward_distributed().unwrap()
6292        };
6293
6294        assert!(
6295            epoch4_last_reward.0 > U256::ZERO,
6296            "epoch 4 last block should have positive rewards"
6297        );
6298
6299        while let Some(leaf) = leaves.next().await {
6300            let leaf = leaf.unwrap();
6301            let header = leaf.header();
6302            let height = header.height();
6303
6304            let total_distributed = header.total_reward_distributed().unwrap();
6305
6306            if height < EPOCH_HEIGHT * 5 {
6307                assert_eq!(total_distributed, epoch4_last_reward,);
6308            } else {
6309                assert!(total_distributed.0 > epoch4_last_reward.0,);
6310                break;
6311            }
6312        }
6313
6314        Ok(())
6315    }
6316
6317    // test actual rewards
6318    // todo: test each account rewards by querying merklized state api
6319    #[test_log::test(tokio::test(flavor = "multi_thread"))]
6320    async fn test_reward_state_v2_epoch_distribution() -> anyhow::Result<()> {
6321        const EPOCH_HEIGHT: u64 = 10;
6322        const NUM_NODES: usize = 5;
6323        const NUM_EPOCHS: u64 = 6;
6324        const V5: Upgrade = Upgrade::trivial(EPOCH_REWARD_VERSION);
6325
6326        let network_config = TestConfigBuilder::default()
6327            .epoch_height(EPOCH_HEIGHT)
6328            .build();
6329
6330        let api_port = reserve_tcp_port().expect("No ports free for query service");
6331
6332        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
6333        let persistence: [_; NUM_NODES] = storage
6334            .iter()
6335            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
6336            .collect::<Vec<_>>()
6337            .try_into()
6338            .unwrap();
6339
6340        let config = TestNetworkConfigBuilder::with_num_nodes()
6341            .api_config(SqlDataSource::options(
6342                &storage[0],
6343                Options::with_port(api_port),
6344            ))
6345            .network_config(network_config)
6346            .persistences(persistence.clone())
6347            .catchups(std::array::from_fn(|_| {
6348                StatePeers::<StaticVersion<0, 1>>::from_urls(
6349                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
6350                    Default::default(),
6351                    Duration::from_secs(2),
6352                    &NoMetrics,
6353                )
6354            }))
6355            .pos_hook(DelegationConfig::MultipleDelegators, Default::default(), V5)
6356            .await
6357            .unwrap()
6358            .build();
6359
6360        let network = TestNetwork::new(config, V5).await;
6361        let client: Client<ClientErr, SequencerApiVersion> =
6362            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
6363
6364        let node_state = network.server.node_state();
6365        let coordinator = node_state.coordinator;
6366
6367        let mut expected_total_distributed = U256::ZERO;
6368
6369        let mut leaves = client
6370            .socket("availability/stream/leaves/0")
6371            .subscribe::<LeafQueryData<SeqTypes>>()
6372            .await
6373            .unwrap();
6374
6375        while let Some(leaf) = leaves.next().await {
6376            let leaf = leaf.unwrap();
6377            let header = leaf.header();
6378            let height = header.height();
6379
6380            let epoch = epoch_from_block_number(height, EPOCH_HEIGHT);
6381
6382            let is_epoch_last_block = height % EPOCH_HEIGHT == 0;
6383
6384            if epoch <= 3 {
6385                continue;
6386            }
6387
6388            let header_total_distributed = header
6389                .total_reward_distributed()
6390                .expect("total_reward_distributed should exist");
6391
6392            if is_epoch_last_block {
6393                let prev_epoch = epoch - 1;
6394                let prev_epoch_number = EpochNumber::new(prev_epoch);
6395                let membership = coordinator.membership();
6396                let prev_block_reward = membership
6397                    .epoch_block_reward(prev_epoch_number)
6398                    .expect("epoch block reward should exist");
6399
6400                let epoch_total = prev_block_reward.0 * U256::from(EPOCH_HEIGHT);
6401                expected_total_distributed += epoch_total;
6402            }
6403
6404            assert_eq!(
6405                header_total_distributed.0, expected_total_distributed,
6406                "total_reward_distributed mismatch at height {height}"
6407            );
6408
6409            if height >= NUM_EPOCHS * EPOCH_HEIGHT {
6410                break;
6411            }
6412        }
6413
6414        Ok(())
6415    }
6416
6417    /// Verifies that the `leader_counts` array in V5+ headers is correct.
6418    #[test_log::test(tokio::test(flavor = "multi_thread"))]
6419    async fn test_epoch_leader_counts() -> anyhow::Result<()> {
6420        const EPOCH_HEIGHT: u64 = 10;
6421        const NUM_NODES: usize = 5;
6422        const NUM_EPOCHS: u64 = 6;
6423        const V5: Upgrade = Upgrade::trivial(EPOCH_REWARD_VERSION);
6424
6425        let network_config = TestConfigBuilder::default()
6426            .epoch_height(EPOCH_HEIGHT)
6427            .build();
6428
6429        let api_port = reserve_tcp_port().expect("No ports free for query service");
6430
6431        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
6432        let persistence: [_; NUM_NODES] = storage
6433            .iter()
6434            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
6435            .collect::<Vec<_>>()
6436            .try_into()
6437            .unwrap();
6438
6439        let config = TestNetworkConfigBuilder::with_num_nodes()
6440            .api_config(SqlDataSource::options(
6441                &storage[0],
6442                Options::with_port(api_port),
6443            ))
6444            .network_config(network_config)
6445            .persistences(persistence.clone())
6446            .catchups(std::array::from_fn(|_| {
6447                StatePeers::<StaticVersion<0, 1>>::from_urls(
6448                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
6449                    Default::default(),
6450                    Duration::from_secs(2),
6451                    &NoMetrics,
6452                )
6453            }))
6454            .pos_hook(DelegationConfig::MultipleDelegators, Default::default(), V5)
6455            .await
6456            .unwrap()
6457            .build();
6458
6459        let network = TestNetwork::new(config, V5).await;
6460        let client: Client<ClientErr, SequencerApiVersion> =
6461            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
6462
6463        let node_state = network.server.node_state();
6464        let coordinator = node_state.coordinator;
6465
6466        // Track expected leader counts by address
6467        let mut expected_counts: HashMap<Address, u16> = HashMap::new();
6468
6469        let mut leaves = client
6470            .socket("availability/stream/leaves/0")
6471            .subscribe::<LeafQueryData<SeqTypes>>()
6472            .await
6473            .unwrap();
6474
6475        while let Some(leaf) = leaves.next().await {
6476            let leaf = leaf.unwrap();
6477            let header = leaf.header();
6478            let height = header.height();
6479            let epoch = epoch_from_block_number(height, EPOCH_HEIGHT);
6480            let epoch_number = EpochNumber::new(epoch);
6481
6482            if epoch <= 2 {
6483                continue;
6484            }
6485
6486            let header_leader_counts = header
6487                .leader_counts()
6488                .expect("V5+ header must have leader_counts");
6489
6490            // Reset counts at the start of a new epoch
6491            let is_epoch_start = (height - 1) % EPOCH_HEIGHT == 0;
6492            if is_epoch_start {
6493                expected_counts.clear();
6494            }
6495
6496            // Determine the leader for this block and track by address.
6497            let view_number = leaf.leaf().view_number();
6498            let snapshot = coordinator
6499                .membership()
6500                .snapshot(epoch_number)
6501                .expect("committee for epoch_number");
6502            let leader = snapshot.leader(view_number).expect("leader should exist");
6503            let leader_address = snapshot
6504                .validator_config(&leader)
6505                .expect("leader should have an address")
6506                .account;
6507
6508            let validator_leader_counts =
6509                ValidatorLeaderCounts::new(&snapshot, *header_leader_counts)
6510                    .expect("ValidatorLeaderCounts should build from header leader_counts");
6511
6512            *expected_counts.entry(leader_address).or_insert(0) += 1;
6513
6514            let header_counts: HashMap<Address, u16> = validator_leader_counts
6515                .active_leaders()
6516                .map(|(v, count)| (v.account, count))
6517                .collect();
6518
6519            assert_eq!(
6520                header_counts, expected_counts,
6521                "leader_counts mismatch at height {height} (epoch {epoch})"
6522            );
6523
6524            if height % EPOCH_HEIGHT == 0 {
6525                let total: u16 = expected_counts.values().sum();
6526                assert_eq!(
6527                    total, EPOCH_HEIGHT as u16,
6528                    "total leader_counts at epoch boundary should equal EPOCH_HEIGHT at height \
6529                     {height}"
6530                );
6531            }
6532
6533            if height >= NUM_EPOCHS * EPOCH_HEIGHT {
6534                break;
6535            }
6536        }
6537
6538        Ok(())
6539    }
6540
6541    #[rstest]
6542    #[case(POS_V3)]
6543    #[case(POS_V4)]
6544    #[test_log::test(tokio::test(flavor = "multi_thread"))]
6545    async fn test_node_stake_table_api(#[case] upgrade: Upgrade) {
6546        let epoch_height = 20;
6547
6548        let network_config = TestConfigBuilder::default()
6549            .epoch_height(epoch_height)
6550            .build();
6551
6552        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
6553
6554        const NUM_NODES: usize = 2;
6555        // Initialize nodes.
6556        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
6557        let persistence: [_; NUM_NODES] = storage
6558            .iter()
6559            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
6560            .collect::<Vec<_>>()
6561            .try_into()
6562            .unwrap();
6563
6564        let config = TestNetworkConfigBuilder::with_num_nodes()
6565            .api_config(SqlDataSource::options(
6566                &storage[0],
6567                Options::with_port(api_port),
6568            ))
6569            .network_config(network_config)
6570            .persistences(persistence.clone())
6571            .catchups(std::array::from_fn(|_| {
6572                StatePeers::<StaticVersion<0, 1>>::from_urls(
6573                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
6574                    Default::default(),
6575                    Duration::from_secs(2),
6576                    &NoMetrics,
6577                )
6578            }))
6579            .pos_hook(
6580                DelegationConfig::MultipleDelegators,
6581                Default::default(),
6582                upgrade,
6583            )
6584            .await
6585            .unwrap()
6586            .build();
6587
6588        let _network = TestNetwork::new(config, upgrade).await;
6589
6590        let client: Client<ClientErr, SequencerApiVersion> =
6591            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
6592
6593        // wait for atleast 2 epochs
6594        let _blocks = client
6595            .socket("availability/stream/blocks/0")
6596            .subscribe::<BlockQueryData<SeqTypes>>()
6597            .await
6598            .unwrap()
6599            .take(40)
6600            .try_collect::<Vec<_>>()
6601            .await
6602            .unwrap();
6603
6604        for i in 1..=3 {
6605            let _st = client
6606                .get::<Vec<PeerConfig<SeqTypes>>>(&format!("node/stake-table/{}", i as u64))
6607                .send()
6608                .await
6609                .expect("failed to get stake table");
6610        }
6611
6612        let _st = client
6613            .get::<StakeTableWithEpochNumber<SeqTypes>>("node/stake-table/current")
6614            .send()
6615            .await
6616            .expect("failed to get stake table");
6617    }
6618
6619    #[rstest]
6620    #[case(POS_V3)]
6621    #[case(POS_V4)]
6622    #[test_log::test(tokio::test(flavor = "multi_thread"))]
6623    async fn test_epoch_stake_table_catchup(#[case] upgrade: Upgrade) {
6624        const EPOCH_HEIGHT: u64 = 10;
6625        const NUM_NODES: usize = 6;
6626
6627        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
6628
6629        let network_config = TestConfigBuilder::default()
6630            .epoch_height(EPOCH_HEIGHT)
6631            .build();
6632
6633        // Initialize storage for each node
6634        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
6635
6636        let persistence_options: [_; NUM_NODES] = storage
6637            .iter()
6638            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
6639            .collect::<Vec<_>>()
6640            .try_into()
6641            .unwrap();
6642
6643        // setup catchup peers
6644        let catchup_peers = std::array::from_fn(|_| {
6645            StatePeers::<StaticVersion<0, 1>>::from_urls(
6646                vec![format!("http://localhost:{port}").parse().unwrap()],
6647                Default::default(),
6648                Duration::from_secs(2),
6649                &NoMetrics,
6650            )
6651        });
6652        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
6653            .api_config(SqlDataSource::options(
6654                &storage[0],
6655                Options::with_port(port),
6656            ))
6657            .network_config(network_config)
6658            .persistences(persistence_options.clone())
6659            .catchups(catchup_peers)
6660            .pos_hook(
6661                DelegationConfig::MultipleDelegators,
6662                Default::default(),
6663                upgrade,
6664            )
6665            .await
6666            .unwrap()
6667            .build();
6668
6669        let state = config.states()[0].clone();
6670        let mut network = TestNetwork::new(config, upgrade).await;
6671
6672        // Wait for the peer 0 (node 1) to advance past three epochs
6673        let mut events = network.peers[0].event_stream();
6674        while let Some(event) = events.next().await {
6675            if let CoordinatorEvent::LegacyEvent(Event {
6676                event: EventType::Decide { leaf_chain, .. },
6677                ..
6678            }) = event
6679            {
6680                let height = leaf_chain[0].leaf.height();
6681                tracing::info!("Node 0 decided at height: {height}");
6682                if height > EPOCH_HEIGHT * 3 {
6683                    break;
6684                }
6685            }
6686        }
6687
6688        // Shutdown and remove node 1 to simulate falling behind
6689        tracing::info!("Shutting down peer 0");
6690        network.peers.remove(0);
6691
6692        // Wait for epochs to progress with node 1 offline
6693        let mut events = network.server.event_stream();
6694        while let Some(event) = events.next().await {
6695            if let CoordinatorEvent::LegacyEvent(Event {
6696                event: EventType::Decide { leaf_chain, .. },
6697                ..
6698            }) = event
6699            {
6700                let height = leaf_chain[0].leaf.height();
6701                if height > EPOCH_HEIGHT * 7 {
6702                    break;
6703                }
6704            }
6705        }
6706
6707        // add node 1 to the network with fresh storage
6708        let storage = SqlDataSource::create_storage().await;
6709        let options = <SqlDataSource as TestableSequencerDataSource>::persistence_options(&storage);
6710        tracing::info!("Restarting peer 0");
6711        let node = network
6712            .cfg
6713            .init_node(
6714                1,
6715                state,
6716                options,
6717                Some(StatePeers::<StaticVersion<0, 1>>::from_urls(
6718                    vec![format!("http://localhost:{port}").parse().unwrap()],
6719                    Default::default(),
6720                    Duration::from_secs(2),
6721                    &NoMetrics,
6722                )),
6723                None,
6724                &NoMetrics,
6725                test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
6726                NullEventConsumer,
6727                upgrade,
6728                Default::default(),
6729            )
6730            .await;
6731
6732        let coordinator = node.node_state().coordinator;
6733        let server_node_state = network.server.node_state();
6734        let server_coordinator = server_node_state.coordinator;
6735        // Verify that the restarted node catches up for each epoch
6736        for epoch_num in 1..=7 {
6737            let epoch = EpochNumber::new(epoch_num);
6738            let node_em = match coordinator.membership_for_epoch(Some(epoch)) {
6739                Ok(em) => em,
6740                Err(_) => coordinator.wait_for_catchup(epoch).await.unwrap(),
6741            };
6742            let server_em = match server_coordinator.membership_for_epoch(Some(epoch)) {
6743                Ok(em) => em,
6744                Err(_) => server_coordinator.wait_for_catchup(epoch).await.unwrap(),
6745            };
6746
6747            println!("have stake table for epoch = {epoch_num}");
6748
6749            let node_stake_table = HSStakeTable::from_iter(node_em.stake_table());
6750            let stake_table = HSStakeTable::from_iter(server_em.stake_table());
6751            println!("asserting stake table for epoch = {epoch_num}");
6752
6753            assert_eq!(
6754                node_stake_table, stake_table,
6755                "Stake table mismatch for epoch {epoch_num}",
6756            );
6757        }
6758    }
6759
6760    #[rstest]
6761    #[case(POS_V3)]
6762    #[case(POS_V4)]
6763    #[test_log::test(tokio::test(flavor = "multi_thread"))]
6764    async fn test_epoch_stake_table_catchup_stress(#[case] upgrade: Upgrade) {
6765        const EPOCH_HEIGHT: u64 = 10;
6766        const NUM_NODES: usize = 6;
6767
6768        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
6769
6770        let network_config = TestConfigBuilder::default()
6771            .epoch_height(EPOCH_HEIGHT)
6772            .build();
6773
6774        // Initialize storage for each node
6775        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
6776
6777        let persistence_options: [_; NUM_NODES] = storage
6778            .iter()
6779            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
6780            .collect::<Vec<_>>()
6781            .try_into()
6782            .unwrap();
6783
6784        // setup catchup peers
6785        let catchup_peers = std::array::from_fn(|_| {
6786            StatePeers::<StaticVersion<0, 1>>::from_urls(
6787                vec![format!("http://localhost:{port}").parse().unwrap()],
6788                Default::default(),
6789                Duration::from_secs(2),
6790                &NoMetrics,
6791            )
6792        });
6793        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
6794            .api_config(SqlDataSource::options(
6795                &storage[0],
6796                Options::with_port(port),
6797            ))
6798            .network_config(network_config)
6799            .persistences(persistence_options.clone())
6800            .catchups(catchup_peers)
6801            .pos_hook(
6802                DelegationConfig::MultipleDelegators,
6803                Default::default(),
6804                upgrade,
6805            )
6806            .await
6807            .unwrap()
6808            .build();
6809
6810        let state = config.states()[0].clone();
6811        let mut network = TestNetwork::new(config, upgrade).await;
6812
6813        // Wait for the peer 0 (node 1) to advance past three epochs
6814        let mut events = network.peers[0].event_stream();
6815        while let Some(event) = events.next().await {
6816            if let CoordinatorEvent::LegacyEvent(Event {
6817                event: EventType::Decide { leaf_chain, .. },
6818                ..
6819            }) = event
6820            {
6821                let height = leaf_chain[0].leaf.height();
6822                tracing::info!("Node 0 decided at height: {height}");
6823                if height > EPOCH_HEIGHT * 3 {
6824                    break;
6825                }
6826            }
6827        }
6828
6829        // Shutdown and remove node 1 to simulate falling behind
6830        tracing::info!("Shutting down peer 0");
6831        network.peers.remove(0);
6832
6833        // Wait for epochs to progress with node 1 offline
6834        let mut events = network.server.event_stream();
6835        while let Some(event) = events.next().await {
6836            if let CoordinatorEvent::LegacyEvent(Event {
6837                event: EventType::Decide { leaf_chain, .. },
6838                ..
6839            }) = event
6840            {
6841                let height = leaf_chain[0].leaf.height();
6842                tracing::info!("Server decided at height: {height}");
6843                //  until 7 epochs
6844                if height > EPOCH_HEIGHT * 7 {
6845                    break;
6846                }
6847            }
6848        }
6849
6850        // add node 1 to the network with fresh storage
6851        let storage = SqlDataSource::create_storage().await;
6852        let options = <SqlDataSource as TestableSequencerDataSource>::persistence_options(&storage);
6853
6854        tracing::info!("Restarting peer 0");
6855        let node = network
6856            .cfg
6857            .init_node(
6858                1,
6859                state,
6860                options,
6861                Some(StatePeers::<StaticVersion<0, 1>>::from_urls(
6862                    vec![format!("http://localhost:{port}").parse().unwrap()],
6863                    Default::default(),
6864                    Duration::from_secs(2),
6865                    &NoMetrics,
6866                )),
6867                None,
6868                &NoMetrics,
6869                test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
6870                NullEventConsumer,
6871                upgrade,
6872                Default::default(),
6873            )
6874            .await;
6875
6876        let coordinator = node.node_state().coordinator;
6877
6878        let server_node_state = network.server.node_state();
6879        let server_coordinator = server_node_state.coordinator;
6880
6881        // Trigger catchup for all epochs in quick succession and in random order
6882        let mut rand_epochs: Vec<_> = (1..=7).collect();
6883        rand_epochs.shuffle(&mut rand::thread_rng());
6884        println!("trigger catchup in this order: {rand_epochs:?}");
6885        for epoch_num in rand_epochs {
6886            let epoch = EpochNumber::new(epoch_num);
6887            let _ = coordinator.membership_for_epoch(Some(epoch));
6888        }
6889
6890        // Verify that the restarted node catches up for each epoch
6891        for epoch_num in 1..=7 {
6892            println!("getting stake table for epoch = {epoch_num}");
6893            let epoch = EpochNumber::new(epoch_num);
6894            let node_em = coordinator.wait_for_catchup(epoch).await.unwrap();
6895            let server_em = match server_coordinator.membership_for_epoch(Some(epoch)) {
6896                Ok(em) => em,
6897                Err(_) => server_coordinator.wait_for_catchup(epoch).await.unwrap(),
6898            };
6899
6900            println!("have stake table for epoch = {epoch_num}");
6901
6902            let node_stake_table = HSStakeTable::from_iter(node_em.stake_table());
6903            let stake_table = HSStakeTable::from_iter(server_em.stake_table());
6904
6905            println!("asserting stake table for epoch = {epoch_num}");
6906
6907            assert_eq!(
6908                node_stake_table, stake_table,
6909                "Stake table mismatch for epoch {epoch_num}",
6910            );
6911        }
6912    }
6913
6914    #[rstest]
6915    #[case(POS_V3)]
6916    #[case(POS_V4)]
6917    #[test_log::test(tokio::test(flavor = "multi_thread"))]
6918    async fn test_merklized_state_catchup_on_restart(
6919        #[case] upgrade: Upgrade,
6920    ) -> anyhow::Result<()> {
6921        // This test verifies that a query node can catch up on
6922        // merklized state after being offline for multiple epochs.
6923        //
6924        // Steps:
6925        // 1. Start a test network with 5 sequencer nodes.
6926        // 2. Start a separate node with the query module enabled, connected to the network.
6927        //    - This node stores merklized state
6928        // 3. Shut down the query node after 1 epoch.
6929        // 4. Allow the network to progress 3 more epochs (query node remains offline).
6930        // 5. Restart the query node.
6931        //    - The node is expected to reconstruct or catch up on its own
6932        use espresso_types::{DECAF_CHAIN_ID, v0_3::ChainConfig};
6933
6934        const EPOCH_HEIGHT: u64 = 10;
6935
6936        let network_config = TestConfigBuilder::default()
6937            .epoch_height(EPOCH_HEIGHT)
6938            .build();
6939
6940        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
6941
6942        tracing::info!("API PORT = {api_port}");
6943        const NUM_NODES: usize = 5;
6944
6945        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
6946        let persistence: [_; NUM_NODES] = storage
6947            .iter()
6948            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
6949            .collect::<Vec<_>>()
6950            .try_into()
6951            .unwrap();
6952
6953        // The light client skips epoch-root stake-table-hash verification for pre-DRB headers only
6954        // on the Decaf chain id, so use it to keep the V3->V4 catchup path covered. Must be set
6955        // before `pos_hook`, which preserves the chain id from `state[0]`.
6956        let decaf_state = ValidatedState {
6957            chain_config: ChainConfig {
6958                chain_id: DECAF_CHAIN_ID,
6959                ..Default::default()
6960            }
6961            .into(),
6962            ..Default::default()
6963        };
6964
6965        let config = TestNetworkConfigBuilder::with_num_nodes()
6966            .api_config(SqlDataSource::options(
6967                &storage[0],
6968                Options::with_port(api_port)
6969                    .catchup(Default::default())
6970                    .light_client(Default::default()),
6971            ))
6972            .network_config(network_config)
6973            .persistences(persistence.clone())
6974            .states(std::array::from_fn(|_| decaf_state.clone()))
6975            .catchups(std::array::from_fn(|_| {
6976                StatePeers::<StaticVersion<0, 1>>::from_urls(
6977                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
6978                    Default::default(),
6979                    Duration::from_secs(2),
6980                    &NoMetrics,
6981                )
6982            }))
6983            .pos_hook(
6984                DelegationConfig::MultipleDelegators,
6985                hotshot_contract_adapter::stake_table::StakeTableContractVersion::V3,
6986                upgrade,
6987            )
6988            .await
6989            .unwrap()
6990            .build();
6991        let state = config.states()[0].clone();
6992        let mut network = TestNetwork::new(config, upgrade).await;
6993
6994        // Remove peer 0 and restart it with the query module enabled.
6995        // Adding an additional node to the test network is not straight forward,
6996        // as the keys have already been initialized in the config above.
6997        // So, we remove this node and re-add it using the same index.
6998        network.peers[0].shut_down().await;
6999        network.peers.remove(0);
7000        let node_0_storage = &storage[1];
7001        let node_0_persistence = persistence[1].clone();
7002        let node_0_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7003        tracing::info!("node_0_port {node_0_port}");
7004        // enable query module with api peers
7005        let opt = Options::with_port(node_0_port).query_sql(
7006            Query {
7007                peers: vec![format!("http://localhost:{api_port}").parse().unwrap()],
7008                ..Default::default()
7009            },
7010            tmp_options(node_0_storage),
7011        );
7012
7013        // start the query node so that it builds the merklized state
7014        let node_0 = opt
7015            .clone()
7016            .serve(|metrics, consumer, storage| {
7017                let cfg = network.cfg.clone();
7018                let node_0_persistence = node_0_persistence.clone();
7019                let state = state.clone();
7020                async move {
7021                    Ok(cfg
7022                        .init_node(
7023                            1,
7024                            state,
7025                            node_0_persistence.clone(),
7026                            Some(StatePeers::<StaticVersion<0, 1>>::from_urls(
7027                                vec![format!("http://localhost:{api_port}").parse().unwrap()],
7028                                Default::default(),
7029                                Duration::from_secs(2),
7030                                &NoMetrics,
7031                            )),
7032                            storage,
7033                            &*metrics,
7034                            test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
7035                            consumer,
7036                            upgrade,
7037                            Default::default(),
7038                        )
7039                        .await)
7040                }
7041                .boxed()
7042            })
7043            .await
7044            .unwrap();
7045
7046        let mut events = network.peers[2].event_stream();
7047        // wait for 1 epoch
7048        wait_for_epochs(&mut events, EPOCH_HEIGHT, 1).await;
7049
7050        // shutdown the node for 3 epochs
7051        drop(node_0);
7052
7053        // wait for 4 epochs
7054        wait_for_epochs(&mut events, EPOCH_HEIGHT, 4).await;
7055
7056        // start the node again.
7057        tracing::info!("restarting node");
7058        let node_0 = opt
7059            .serve(|metrics, consumer, storage| {
7060                let cfg = network.cfg.clone();
7061                async move {
7062                    Ok(cfg
7063                        .init_node(
7064                            1,
7065                            state,
7066                            node_0_persistence,
7067                            Some(StatePeers::<StaticVersion<0, 1>>::from_urls(
7068                                vec![format!("http://localhost:{api_port}").parse().unwrap()],
7069                                Default::default(),
7070                                Duration::from_secs(2),
7071                                &NoMetrics,
7072                            )),
7073                            storage,
7074                            &*metrics,
7075                            test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
7076                            consumer,
7077                            upgrade,
7078                            Default::default(),
7079                        )
7080                        .await)
7081                }
7082                .boxed()
7083            })
7084            .await
7085            .unwrap();
7086
7087        let client: Client<ClientErr, SequencerApiVersion> =
7088            Client::new(format!("http://localhost:{node_0_port}").parse().unwrap());
7089        client.connect(None).await;
7090
7091        wait_for_epochs(&mut events, EPOCH_HEIGHT, 6).await;
7092
7093        let epoch_7_block = EPOCH_HEIGHT * 6 + 1;
7094
7095        // check that the node's state has reward accounts
7096        let mut retries = 0;
7097        loop {
7098            sleep(Duration::from_secs(1)).await;
7099            let state = node_0.decided_state().await.unwrap();
7100
7101            let leaves = if upgrade.base == EPOCH_VERSION {
7102                // Use legacy tree for V3
7103                state.reward_merkle_tree_v1.num_leaves()
7104            } else {
7105                // Use new tree for V4 and above
7106                state.reward_merkle_tree_v2.num_leaves()
7107            };
7108
7109            if leaves > 0 {
7110                tracing::info!("Node's state has reward accounts");
7111                break;
7112            }
7113
7114            retries += 1;
7115            if retries > 120 {
7116                panic!("max retries reached. failed to catchup reward state");
7117            }
7118        }
7119
7120        retries = 0;
7121        // check that the node has stored atleast 6 epochs merklized state in persistence
7122        loop {
7123            sleep(Duration::from_secs(3)).await;
7124
7125            let bh = client
7126                .get::<u64>("block-state/block-height")
7127                .send()
7128                .await
7129                .expect("block height not found");
7130
7131            tracing::info!("block state: block height={bh}");
7132            if bh > epoch_7_block {
7133                break;
7134            }
7135
7136            retries += 1;
7137            if retries > 30 {
7138                panic!(
7139                    "max retries reached. block state block height is less than epoch 7 start \
7140                     block"
7141                );
7142            }
7143        }
7144
7145        // shutdown consensus to freeze the state
7146        node_0.shutdown_consensus().await;
7147        let decided_leaf = node_0.decided_leaf().await;
7148        let state = node_0.decided_state().await.unwrap();
7149        tracing::info!(
7150            height = decided_leaf.height(),
7151            ?decided_leaf,
7152            ?state,
7153            "final state"
7154        );
7155
7156        let height = decided_leaf.height();
7157        let num_leaves = state.block_merkle_tree.num_leaves();
7158        tracing::info!(height, num_leaves, "checking block merkle tree state");
7159        state
7160            .block_merkle_tree
7161            .lookup(height - 1)
7162            .expect_ok()
7163            .unwrap_or_else(|err| {
7164                panic!(
7165                    "block state not found ({err:#}):\n{:#?}",
7166                    state.block_merkle_tree
7167                )
7168            });
7169
7170        Ok(())
7171    }
7172
7173    #[rstest]
7174    #[case(POS_V4)]
7175    #[test_log::test(tokio::test(flavor = "multi_thread"))]
7176    async fn test_state_reconstruction(#[case] upgrade: Upgrade) -> anyhow::Result<()> {
7177        // This test verifies that a query node can successfully reconstruct its state
7178        // after being shut down from the database
7179        //
7180        // Steps:
7181        // 1. Start a test network with 5 nodes.
7182        // 2. Add a query node connected to the network.
7183        // 3. Let the network run until 3 epochs have passed.
7184        // 4. Shut down the query node.
7185        // 5. Attempt to reconstruct its state from storage using:
7186        //    - No fee/reward accounts
7187        //    - Only fee accounts
7188        //    - Only reward accounts
7189        //    - Both fee and reward accounts
7190        // 6. Assert that the reconstructed state is correct in all scenarios.
7191
7192        const EPOCH_HEIGHT: u64 = 10;
7193
7194        let network_config = TestConfigBuilder::default()
7195            .epoch_height(EPOCH_HEIGHT)
7196            .build();
7197
7198        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7199
7200        tracing::info!("API PORT = {api_port}");
7201        const NUM_NODES: usize = 5;
7202
7203        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
7204        let persistence: [_; NUM_NODES] = storage
7205            .iter()
7206            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
7207            .collect::<Vec<_>>()
7208            .try_into()
7209            .unwrap();
7210
7211        let config = TestNetworkConfigBuilder::with_num_nodes()
7212            .api_config(SqlDataSource::options(
7213                &storage[0],
7214                Options::with_port(api_port).light_client(Default::default()),
7215            ))
7216            .network_config(network_config)
7217            .persistences(persistence.clone())
7218            .catchups(std::array::from_fn(|_| {
7219                StatePeers::<StaticVersion<0, 1>>::from_urls(
7220                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
7221                    Default::default(),
7222                    Duration::from_secs(2),
7223                    &NoMetrics,
7224                )
7225            }))
7226            .pos_hook(
7227                DelegationConfig::MultipleDelegators,
7228                hotshot_contract_adapter::stake_table::StakeTableContractVersion::V3,
7229                upgrade,
7230            )
7231            .await
7232            .unwrap()
7233            .build();
7234        let state = config.states()[0].clone();
7235        let mut network = TestNetwork::new(config, upgrade).await;
7236        // Remove peer 0 and restart it with the query module enabled.
7237        // Adding an additional node to the test network is not straight forward,
7238        // as the keys have already been initialized in the config above.
7239        // So, we remove this node and re-add it using the same index.
7240        // Await the shutdown so the node's cliquenet listener releases its
7241        // port before the replacement node binds the same address.
7242        network.peers[0].shut_down().await;
7243        network.peers.remove(0);
7244
7245        let node_0_storage = &storage[1];
7246        let node_0_persistence = persistence[1].clone();
7247        let node_0_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7248        tracing::info!("node_0_port {node_0_port}");
7249        let opt = Options::with_port(node_0_port).query_sql(
7250            Query {
7251                peers: vec![format!("http://localhost:{api_port}").parse().unwrap()],
7252                ..Query::test()
7253            },
7254            tmp_options(node_0_storage),
7255        );
7256        let node_0 = opt
7257            .clone()
7258            .serve(|metrics, consumer, storage| {
7259                let cfg = network.cfg.clone();
7260                let node_0_persistence = node_0_persistence.clone();
7261                let state = state.clone();
7262                async move {
7263                    Ok(cfg
7264                        .init_node(
7265                            1,
7266                            state,
7267                            node_0_persistence.clone(),
7268                            Some(StatePeers::<StaticVersion<0, 1>>::from_urls(
7269                                vec![format!("http://localhost:{api_port}").parse().unwrap()],
7270                                Default::default(),
7271                                Duration::from_secs(2),
7272                                &NoMetrics,
7273                            )),
7274                            storage,
7275                            &*metrics,
7276                            test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
7277                            consumer,
7278                            upgrade,
7279                            Default::default(),
7280                        )
7281                        .await)
7282                }
7283                .boxed()
7284            })
7285            .await
7286            .unwrap();
7287
7288        let mut events = network.peers[2].event_stream();
7289        // Wait until at least 3 epochs have passed
7290        wait_for_epochs(&mut events, EPOCH_HEIGHT, 3).await;
7291
7292        tracing::warn!("shutting down node 0");
7293
7294        node_0.shutdown_consensus().await;
7295
7296        let instance = node_0.node_state();
7297        let state = node_0.decided_state().await.unwrap();
7298        let fee_accounts = state
7299            .fee_merkle_tree
7300            .clone()
7301            .into_iter()
7302            .map(|(acct, _)| acct)
7303            .collect::<Vec<_>>();
7304        let reward_accounts = match upgrade.base {
7305            EPOCH_VERSION => state
7306                .reward_merkle_tree_v1
7307                .clone()
7308                .into_iter()
7309                .map(|(acct, _)| RewardAccountV2::from(acct))
7310                .collect::<Vec<_>>(),
7311            DRB_AND_HEADER_UPGRADE_VERSION => state
7312                .reward_merkle_tree_v2
7313                .clone()
7314                .into_iter()
7315                .map(|(acct, _)| acct)
7316                .collect::<Vec<_>>(),
7317            _ => panic!("invalid version"),
7318        };
7319
7320        let client: Client<ClientErr, SequencerApiVersion> =
7321            Client::new(format!("http://localhost:{node_0_port}").parse().unwrap());
7322        client.connect(Some(Duration::from_secs(10))).await;
7323
7324        // wait 3s to be sure that all the
7325        // transactions have been committed
7326        sleep(Duration::from_secs(3)).await;
7327
7328        tracing::info!("getting node block height");
7329        let node_block_height = client
7330            .get::<u64>("node/block-height")
7331            .send()
7332            .await
7333            .context("getting Espresso block height")
7334            .unwrap();
7335
7336        tracing::info!("node block height={node_block_height}");
7337
7338        let leaf_query_data = client
7339            .get::<LeafQueryData<SeqTypes>>(&format!("availability/leaf/{}", node_block_height - 1))
7340            .send()
7341            .await
7342            .context("error getting leaf")
7343            .unwrap();
7344
7345        tracing::info!("leaf={leaf_query_data:?}");
7346        let leaf = leaf_query_data.leaf();
7347        let to_view = leaf.view_number() + 1;
7348
7349        let ds = SqlStorage::connect(
7350            Config::try_from(&node_0_persistence).unwrap(),
7351            StorageConnectionType::Sequencer,
7352        )
7353        .await
7354        .unwrap();
7355        let mut tx = ds.read().await?;
7356
7357        let (state, leaf) = reconstruct_state(
7358            &instance,
7359            &ds,
7360            &mut tx,
7361            node_block_height - 1,
7362            to_view,
7363            &[],
7364            &[],
7365        )
7366        .await
7367        .unwrap();
7368        assert_eq!(leaf.view_number(), to_view);
7369        assert!(
7370            state
7371                .block_merkle_tree
7372                .lookup(node_block_height - 1)
7373                .expect_ok()
7374                .is_ok(),
7375            "inconsistent block merkle tree"
7376        );
7377
7378        // Reconstruct fee state
7379        let (state, leaf) = reconstruct_state(
7380            &instance,
7381            &ds,
7382            &mut tx,
7383            node_block_height - 1,
7384            to_view,
7385            &fee_accounts,
7386            &[],
7387        )
7388        .await
7389        .unwrap();
7390
7391        assert_eq!(leaf.view_number(), to_view);
7392        assert!(
7393            state
7394                .block_merkle_tree
7395                .lookup(node_block_height - 1)
7396                .expect_ok()
7397                .is_ok(),
7398            "inconsistent block merkle tree"
7399        );
7400
7401        for account in &fee_accounts {
7402            state.fee_merkle_tree.lookup(account).expect_ok().unwrap();
7403        }
7404
7405        // Reconstruct reward state
7406
7407        let (state, leaf) = reconstruct_state(
7408            &instance,
7409            &ds,
7410            &mut tx,
7411            node_block_height - 1,
7412            to_view,
7413            &[],
7414            &reward_accounts,
7415        )
7416        .await
7417        .unwrap();
7418
7419        match upgrade.base {
7420            EPOCH_VERSION => {
7421                for account in reward_accounts.clone() {
7422                    state
7423                        .reward_merkle_tree_v1
7424                        .lookup(RewardAccountV1::from(account))
7425                        .expect_ok()
7426                        .unwrap();
7427                }
7428            },
7429            DRB_AND_HEADER_UPGRADE_VERSION => {
7430                for account in &reward_accounts {
7431                    state
7432                        .reward_merkle_tree_v2
7433                        .lookup(account)
7434                        .expect_ok()
7435                        .unwrap();
7436                }
7437            },
7438            _ => panic!("invalid version"),
7439        };
7440
7441        assert_eq!(leaf.view_number(), to_view);
7442        assert!(
7443            state
7444                .block_merkle_tree
7445                .lookup(node_block_height - 1)
7446                .expect_ok()
7447                .is_ok(),
7448            "inconsistent block merkle tree"
7449        );
7450        // Reconstruct reward and fee state
7451
7452        let (state, leaf) = reconstruct_state(
7453            &instance,
7454            &ds,
7455            &mut tx,
7456            node_block_height - 1,
7457            to_view,
7458            &fee_accounts,
7459            &reward_accounts,
7460        )
7461        .await
7462        .unwrap();
7463
7464        assert!(
7465            state
7466                .block_merkle_tree
7467                .lookup(node_block_height - 1)
7468                .expect_ok()
7469                .is_ok(),
7470            "inconsistent block merkle tree"
7471        );
7472        assert_eq!(leaf.view_number(), to_view);
7473
7474        match upgrade.base {
7475            EPOCH_VERSION => {
7476                for account in reward_accounts.clone() {
7477                    state
7478                        .reward_merkle_tree_v1
7479                        .lookup(RewardAccountV1::from(account))
7480                        .expect_ok()
7481                        .unwrap();
7482                }
7483            },
7484            DRB_AND_HEADER_UPGRADE_VERSION => {
7485                for account in &reward_accounts {
7486                    state
7487                        .reward_merkle_tree_v2
7488                        .lookup(account)
7489                        .expect_ok()
7490                        .unwrap();
7491                }
7492            },
7493            _ => panic!("invalid version"),
7494        };
7495
7496        for account in &fee_accounts {
7497            state.fee_merkle_tree.lookup(account).expect_ok().unwrap();
7498        }
7499
7500        Ok(())
7501    }
7502
7503    #[rstest]
7504    #[case(POS_V3)]
7505    #[case(POS_V4)]
7506    #[test_log::test(tokio::test(flavor = "multi_thread"))]
7507    async fn test_block_reward_api(#[case] upgrade: Upgrade) -> anyhow::Result<()> {
7508        let epoch_height = 10;
7509
7510        let network_config = TestConfigBuilder::default()
7511            .epoch_height(epoch_height)
7512            .build();
7513
7514        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7515
7516        const NUM_NODES: usize = 1;
7517        // Initialize nodes.
7518        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
7519        let persistence: [_; NUM_NODES] = storage
7520            .iter()
7521            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
7522            .collect::<Vec<_>>()
7523            .try_into()
7524            .unwrap();
7525
7526        let config = TestNetworkConfigBuilder::with_num_nodes()
7527            .api_config(SqlDataSource::options(
7528                &storage[0],
7529                Options::with_port(api_port),
7530            ))
7531            .network_config(network_config.clone())
7532            .persistences(persistence.clone())
7533            .catchups(std::array::from_fn(|_| {
7534                StatePeers::<StaticVersion<0, 1>>::from_urls(
7535                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
7536                    Default::default(),
7537                    Duration::from_secs(2),
7538                    &NoMetrics,
7539                )
7540            }))
7541            .pos_hook(
7542                DelegationConfig::VariableAmounts,
7543                Default::default(),
7544                upgrade,
7545            )
7546            .await
7547            .unwrap()
7548            .build();
7549
7550        let _network = TestNetwork::new(config, upgrade).await;
7551        let client: Client<ClientErr, SequencerApiVersion> =
7552            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
7553
7554        let _blocks = client
7555            .socket("availability/stream/blocks/0")
7556            .subscribe::<BlockQueryData<SeqTypes>>()
7557            .await
7558            .unwrap()
7559            .take(3)
7560            .try_collect::<Vec<_>>()
7561            .await
7562            .unwrap();
7563
7564        let block_reward = client
7565            .get::<Option<RewardAmount>>("node/block-reward")
7566            .send()
7567            .await
7568            .expect("failed to get block reward")
7569            .expect("block reward is None");
7570        tracing::info!("block_reward={block_reward:?}");
7571
7572        assert!(block_reward.0 > U256::ZERO);
7573
7574        Ok(())
7575    }
7576
7577    /// `chain_id`: None = default (35353, non-mainnet), Some(1) = mainnet
7578    #[rstest]
7579    #[case(POS_V4, None)]
7580    #[case(POS_V4, Some(1u64))]
7581    #[test_log::test(tokio::test(flavor = "multi_thread"))]
7582    async fn test_token_supply_api(
7583        #[case] upgrade: Upgrade,
7584        #[case] chain_id: Option<u64>,
7585    ) -> anyhow::Result<()> {
7586        use alloy::primitives::utils::parse_ether;
7587        use espresso_types::v0_3::ChainConfig;
7588
7589        let epoch_height = 10;
7590        let network_config = TestConfigBuilder::default()
7591            .epoch_height(epoch_height)
7592            .build();
7593
7594        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7595
7596        const NUM_NODES: usize = 1;
7597        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
7598        let persistence: [_; NUM_NODES] = storage
7599            .iter()
7600            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
7601            .collect::<Vec<_>>()
7602            .try_into()
7603            .unwrap();
7604
7605        // Use the real initial supply (3.59B tokens) so the unlock schedule
7606        // produces realistic locked/unlocked values in the supply calculations.
7607        let initial_supply_tokens = U256::from(3_590_000_000u64);
7608        let initial_supply_wei = parse_ether("3590000000").unwrap();
7609
7610        let mut builder = TestNetworkConfigBuilder::with_num_nodes()
7611            .api_config(SqlDataSource::options(
7612                &storage[0],
7613                Options::with_port(api_port),
7614            ))
7615            .network_config(network_config.clone())
7616            .persistences(persistence.clone())
7617            .catchups(std::array::from_fn(|_| {
7618                StatePeers::<StaticVersion<0, 1>>::from_urls(
7619                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
7620                    Default::default(),
7621                    Duration::from_secs(2),
7622                    &NoMetrics,
7623                )
7624            }))
7625            .initial_token_supply(initial_supply_tokens);
7626
7627        // Must set states before pos_hook, which preserves chain_id from state[0].
7628        if let Some(id) = chain_id {
7629            let state = ValidatedState {
7630                chain_config: ChainConfig {
7631                    chain_id: U256::from(id).into(),
7632                    ..Default::default()
7633                }
7634                .into(),
7635                ..Default::default()
7636            };
7637            builder = builder.states(std::array::from_fn(|_| state.clone()));
7638        }
7639
7640        let config = builder
7641            .pos_hook(
7642                DelegationConfig::VariableAmounts,
7643                Default::default(),
7644                upgrade,
7645            )
7646            .await
7647            .unwrap()
7648            .build();
7649
7650        let _network = TestNetwork::new(config, upgrade).await;
7651        let client: Client<ClientErr, SequencerApiVersion> =
7652            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
7653
7654        let _blocks = client
7655            .socket("availability/stream/blocks/0")
7656            .subscribe::<BlockQueryData<SeqTypes>>()
7657            .await
7658            .unwrap()
7659            .take(3)
7660            .try_collect::<Vec<_>>()
7661            .await
7662            .unwrap();
7663
7664        let minted: String = client
7665            .get("token/total-minted-supply")
7666            .send()
7667            .await
7668            .expect("total-minted-supply");
7669        let circ_eth: String = client
7670            .get("token/circulating-supply-ethereum")
7671            .send()
7672            .await
7673            .expect("circulating-supply-ethereum");
7674        let circulating: String = client
7675            .get("token/circulating-supply")
7676            .send()
7677            .await
7678            .expect("circulating-supply");
7679        tracing::info!(%minted, %circ_eth, %circulating);
7680
7681        let minted = parse_ether(&minted)?;
7682        let circ_eth = parse_ether(&circ_eth)?;
7683        let circ = parse_ether(&circulating)?;
7684
7685        assert_eq!(minted, initial_supply_wei);
7686        assert!(circ_eth <= minted);
7687        assert!(circ >= circ_eth);
7688        assert!(circ > U256::ZERO);
7689
7690        if chain_id == Some(1) {
7691            // Proves the unlock schedule is hooked up: locked > 0 means
7692            // the mainnet code path ran. Vesting ends ~2032; delete after.
7693            assert!(circ_eth < minted);
7694        }
7695
7696        Ok(())
7697    }
7698
7699    #[test_log::test(tokio::test(flavor = "multi_thread"))]
7700    async fn test_scanning_token_contract_initialized_event() -> anyhow::Result<()> {
7701        use espresso_types::v0_3::ChainConfig;
7702
7703        let blocks_per_epoch = 10;
7704
7705        let network_config = TestConfigBuilder::<1>::default()
7706            .epoch_height(blocks_per_epoch)
7707            .build();
7708
7709        let (genesis_state, genesis_stake) = light_client_genesis_from_stake_table(
7710            &network_config.hotshot_config().hotshot_stake_table(),
7711            STAKE_TABLE_CAPACITY_FOR_TEST,
7712        )
7713        .unwrap();
7714
7715        let deployer = ProviderBuilder::new()
7716            .wallet(EthereumWallet::from(network_config.signer().clone()))
7717            .connect_http(network_config.l1_url().clone());
7718
7719        let mut contracts = Contracts::new();
7720        let args = DeployerArgsBuilder::default()
7721            .deployer(deployer.clone())
7722            .rpc_url(network_config.l1_url().clone())
7723            .mock_light_client(true)
7724            .genesis_lc_state(genesis_state)
7725            .genesis_st_state(genesis_stake)
7726            .blocks_per_epoch(blocks_per_epoch)
7727            .epoch_start_block(1)
7728            .multisig_pauser(network_config.signer().address())
7729            .token_name("Espresso".to_string())
7730            .token_symbol("ESP".to_string())
7731            .initial_token_supply(U256::from(3590000000u64))
7732            .ops_timelock_delay(U256::from(0))
7733            .ops_timelock_admin(network_config.signer().address())
7734            .ops_timelock_proposers(vec![network_config.signer().address()])
7735            .ops_timelock_executors(vec![network_config.signer().address()])
7736            .safe_exit_timelock_delay(U256::from(0))
7737            .safe_exit_timelock_admin(network_config.signer().address())
7738            .safe_exit_timelock_proposers(vec![network_config.signer().address()])
7739            .safe_exit_timelock_executors(vec![network_config.signer().address()])
7740            .build()
7741            .unwrap();
7742
7743        args.deploy_to_stake_table_v3(&mut contracts).await.unwrap();
7744
7745        let st_addr = contracts
7746            .address(Contract::StakeTableProxy)
7747            .expect("StakeTableProxy deployed");
7748
7749        let l1_url = network_config.l1_url().clone();
7750
7751        let storage = SqlDataSource::create_storage().await;
7752        let mut opt = <SqlDataSource as TestableSequencerDataSource>::persistence_options(&storage);
7753        let persistence = opt.create().await.unwrap();
7754
7755        let l1_client = L1ClientOptions {
7756            stake_table_update_interval: Duration::from_secs(7),
7757            l1_retry_delay: Duration::from_millis(10),
7758            l1_events_max_block_range: 10000,
7759            ..Default::default()
7760        }
7761        .connect(vec![l1_url])
7762        .unwrap();
7763        l1_client.spawn_tasks().await;
7764
7765        let fetcher = Fetcher::new(
7766            Arc::new(NullStateCatchup::default()),
7767            Arc::new(Mutex::new(persistence.clone())),
7768            l1_client.clone(),
7769            ChainConfig {
7770                stake_table_contract: Some(st_addr),
7771                base_fee: 0.into(),
7772                ..Default::default()
7773            },
7774        );
7775
7776        let provider = l1_client.provider;
7777        let stake_table = StakeTableV3::new(st_addr, provider.clone());
7778
7779        let stake_table_init_block = stake_table
7780            .initializedAtBlock()
7781            .block(BlockId::finalized())
7782            .call()
7783            .await?
7784            .to::<u64>();
7785
7786        tracing::info!("stake table init block = {stake_table_init_block}");
7787
7788        let token_address = stake_table
7789            .token()
7790            .block(BlockId::finalized())
7791            .call()
7792            .await
7793            .context("Failed to get token address")?;
7794
7795        let token = EspToken::new(token_address, provider.clone());
7796
7797        let init_log = fetcher
7798            .scan_token_contract_initialized_event_log(stake_table_init_block, token.clone())
7799            .await
7800            .unwrap();
7801
7802        let init_block = init_log.block_number.context("missing block number")?;
7803        let init_tx_hash = init_log
7804            .transaction_hash
7805            .context("missing transaction hash")?;
7806
7807        let transfer_logs = token
7808            .Transfer_filter()
7809            .from_block(init_block)
7810            .to_block(init_block)
7811            .query()
7812            .await
7813            .unwrap();
7814
7815        let (mint_transfer, _) = transfer_logs
7816            .iter()
7817            .find(|(transfer, log)| {
7818                log.transaction_hash == Some(init_tx_hash) && transfer.from == Address::ZERO
7819            })
7820            .context("no mint transfer event in init tx")?;
7821
7822        assert!(mint_transfer.value > U256::ZERO);
7823
7824        Ok(())
7825    }
7826
7827    #[test_log::test(tokio::test(flavor = "multi_thread"))]
7828    async fn test_tx_metadata() {
7829        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7830
7831        let url = format!("http://localhost:{port}").parse().unwrap();
7832        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
7833
7834        let storage = SqlDataSource::create_storage().await;
7835        let network_config = TestConfigBuilder::default().build();
7836        let config = TestNetworkConfigBuilder::default()
7837            .api_config(
7838                SqlDataSource::options(&storage, Options::with_port(port))
7839                    .submit(Default::default())
7840                    .explorer(Default::default()),
7841            )
7842            .network_config(network_config)
7843            .build();
7844        let network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
7845        let mut events = network.server.event_stream();
7846
7847        client.connect(None).await;
7848
7849        // Submit a few transactions in different namespaces.
7850        let namespace_counts = [(101, 1), (102, 2), (103, 3)];
7851        for (ns, count) in &namespace_counts {
7852            for i in 0..*count {
7853                let ns_id = NamespaceId::from(*ns as u64);
7854                let txn = Transaction::new(ns_id, vec![*ns, i]);
7855                client
7856                    .post::<()>("submit/submit")
7857                    .body_json(&txn)
7858                    .unwrap()
7859                    .send()
7860                    .await
7861                    .unwrap();
7862                let (block, _) = wait_for_decide_on_handle(&mut events, &txn).await;
7863
7864                // Block summary should contain information about the namespace.
7865                let summary: BlockSummaryQueryData<SeqTypes> = client
7866                    .get(&format!("availability/block/summary/{block}"))
7867                    .send()
7868                    .await
7869                    .unwrap();
7870                let ns_info = summary.namespaces();
7871                assert_eq!(ns_info.len(), 1);
7872                assert_eq!(ns_info.keys().copied().collect::<Vec<_>>(), vec![ns_id]);
7873                assert_eq!(ns_info[&ns_id].num_transactions, 1);
7874                assert_eq!(ns_info[&ns_id].size, txn.size_in_block(true));
7875            }
7876        }
7877
7878        // List transactions in each namespace.
7879        for (ns, count) in &namespace_counts {
7880            tracing::info!(ns, "list transactions in namespace");
7881
7882            let ns_id = NamespaceId::from(*ns as u64);
7883            let summaries: TransactionSummariesResponse<SeqTypes> = client
7884                .get(&format!(
7885                    "explorer/transactions/latest/{count}/namespace/{ns_id}"
7886                ))
7887                .send()
7888                .await
7889                .unwrap();
7890            let txs = summaries.transaction_summaries;
7891            assert_eq!(txs.len(), *count as usize);
7892
7893            // Check that transactions are listed in descending order.
7894            for i in 0..*count {
7895                let summary = &txs[i as usize];
7896                let expected = Transaction::new(ns_id, vec![*ns, count - i - 1]);
7897                assert_eq!(summary.rollups, vec![ns_id]);
7898                assert_eq!(summary.hash, expected.commit());
7899            }
7900        }
7901    }
7902
7903    use rand::thread_rng;
7904
7905    #[test_log::test(tokio::test(flavor = "multi_thread"))]
7906    async fn test_aggregator_namespace_endpoints() {
7907        let mut rng = thread_rng();
7908
7909        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7910
7911        let url = format!("http://localhost:{port}").parse().unwrap();
7912        tracing::info!("Sequencer URL = {url}");
7913        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
7914
7915        let options = Options::with_port(port).submit(Default::default());
7916        const NUM_NODES: usize = 2;
7917        // Initialize storage for each node
7918        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
7919
7920        let persistence_options: [_; NUM_NODES] = storage
7921            .iter()
7922            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
7923            .collect::<Vec<_>>()
7924            .try_into()
7925            .unwrap();
7926
7927        let network_config = TestConfigBuilder::default().build();
7928
7929        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
7930            .api_config(SqlDataSource::options(&storage[0], options))
7931            .network_config(network_config)
7932            .persistences(persistence_options.clone())
7933            .build();
7934        let network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
7935        let mut events = network.server.event_stream();
7936        let start = Instant::now();
7937        let mut total_transactions = 0;
7938        let mut tx_heights = Vec::new();
7939        let mut sizes = HashMap::new();
7940        // inserting transactions for some namespaces
7941        // the number of transactions inserted is equal to namespace number.
7942        for namespace in 1..=4 {
7943            for _count in 0..namespace {
7944                // Generate a random payload length between 4 and 10 bytes
7945                let payload_len = rng.gen_range(4..=10);
7946                let payload: Vec<u8> = (0..payload_len).map(|_| rng.r#gen()).collect();
7947
7948                let txn = Transaction::new(NamespaceId::from(namespace as u32), payload);
7949
7950                client.connect(None).await;
7951
7952                let hash = client
7953                    .post("submit/submit")
7954                    .body_json(&txn)
7955                    .unwrap()
7956                    .send()
7957                    .await
7958                    .unwrap();
7959                assert_eq!(txn.commit(), hash);
7960
7961                // Wait for a Decide event containing transaction matching the one we sent
7962                let (height, size) = wait_for_decide_on_handle(&mut events, &txn).await;
7963                tx_heights.push(height);
7964                total_transactions += 1;
7965                *sizes.entry(namespace).or_insert(0) += size;
7966            }
7967        }
7968
7969        let duration = start.elapsed();
7970
7971        println!("Time elapsed to submit transactions: {duration:?}");
7972
7973        let last_tx_height = tx_heights.last().unwrap();
7974
7975        // Decide events fire when consensus decides a block, but the aggregator that backs these
7976        // endpoints runs as a separate background task. Wait for it to have written rows up to
7977        // last_tx_height before asserting; otherwise queries can hit a not-yet-aggregated height
7978        // and 404.
7979        let aggregator_deadline = Instant::now() + Duration::from_secs(30);
7980        loop {
7981            let count = client
7982                .get::<u64>(&format!("node/transactions/count/{last_tx_height}"))
7983                .send()
7984                .await
7985                .ok();
7986            if count == Some(total_transactions) {
7987                break;
7988            }
7989            assert!(
7990                Instant::now() < aggregator_deadline,
7991                "aggregator did not catch up to height {last_tx_height} (got {count:?}, expected \
7992                 {total_transactions})"
7993            );
7994            sleep(Duration::from_secs(1)).await;
7995        }
7996
7997        for namespace in 1..=4 {
7998            let count = client
7999                .get::<u64>(&format!("node/transactions/count/namespace/{namespace}"))
8000                .send()
8001                .await
8002                .unwrap();
8003            assert_eq!(
8004                count, namespace as u64,
8005                "Incorrect transaction count for namespace {namespace}: expected {namespace}, got \
8006                 {count}"
8007            );
8008
8009            // check the range endpoint
8010            let to_endpoint_count = client
8011                .get::<u64>(&format!(
8012                    "node/transactions/count/namespace/{namespace}/{last_tx_height}"
8013                ))
8014                .send()
8015                .await
8016                .unwrap();
8017            assert_eq!(
8018                to_endpoint_count, namespace as u64,
8019                "Incorrect transaction count for range endpoint (to only) for namespace \
8020                 {namespace}: expected {namespace}, got {to_endpoint_count}"
8021            );
8022
8023            // check the range endpoint
8024            let from_to_endpoint_count = client
8025                .get::<u64>(&format!(
8026                    "node/transactions/count/namespace/{namespace}/0/{last_tx_height}"
8027                ))
8028                .send()
8029                .await
8030                .unwrap();
8031            assert_eq!(
8032                from_to_endpoint_count, namespace as u64,
8033                "Incorrect transaction count for range endpoint (from-to) for namespace \
8034                 {namespace}: expected {namespace}, got {from_to_endpoint_count}"
8035            );
8036
8037            let ns_size = client
8038                .get::<usize>(&format!("node/payloads/size/namespace/{namespace}"))
8039                .send()
8040                .await
8041                .unwrap();
8042
8043            let expected_ns_size = *sizes.get(&namespace).unwrap();
8044            assert_eq!(
8045                ns_size, expected_ns_size,
8046                "Incorrect payload size for namespace {namespace}: expected {expected_ns_size}, \
8047                 got {ns_size}"
8048            );
8049
8050            let ns_size_to = client
8051                .get::<usize>(&format!(
8052                    "node/payloads/size/namespace/{namespace}/{last_tx_height}"
8053                ))
8054                .send()
8055                .await
8056                .unwrap();
8057            assert_eq!(
8058                ns_size_to, expected_ns_size,
8059                "Incorrect payload size for namespace {namespace} up to height {last_tx_height}: \
8060                 expected {expected_ns_size}, got {ns_size_to}"
8061            );
8062
8063            let ns_size_from_to = client
8064                .get::<usize>(&format!(
8065                    "node/payloads/size/namespace/{namespace}/0/{last_tx_height}"
8066                ))
8067                .send()
8068                .await
8069                .unwrap();
8070            assert_eq!(
8071                ns_size_from_to, expected_ns_size,
8072                "Incorrect payload size for namespace {namespace} from 0 to height \
8073                 {last_tx_height}: expected {expected_ns_size}, got {ns_size_from_to}"
8074            );
8075        }
8076
8077        let total_tx_count = client
8078            .get::<u64>("node/transactions/count")
8079            .send()
8080            .await
8081            .unwrap();
8082        assert_eq!(
8083            total_tx_count, total_transactions,
8084            "Incorrect total transaction count: expected {total_transactions}, got \
8085             {total_tx_count}"
8086        );
8087
8088        let total_payload_size = client
8089            .get::<usize>("node/payloads/size")
8090            .send()
8091            .await
8092            .unwrap();
8093
8094        let expected_total_size: usize = sizes.values().copied().sum();
8095        assert_eq!(
8096            total_payload_size, expected_total_size,
8097            "Incorrect total payload size: expected {expected_total_size}, got \
8098             {total_payload_size}"
8099        );
8100    }
8101
8102    #[test_log::test(tokio::test(flavor = "multi_thread"))]
8103    async fn test_stream_transactions_endpoint() {
8104        // This test submits transactions to a sequencer for multiple namespaces,
8105        // waits for them to be decided, and then verifies that:
8106        // 1. All transactions appear in the transaction stream.
8107        // 2. Each namespace-specific transaction stream only includes the transactions of that namespace.
8108
8109        let mut rng = thread_rng();
8110
8111        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
8112
8113        let url = format!("http://localhost:{port}").parse().unwrap();
8114        tracing::info!("Sequencer URL = {url}");
8115        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
8116
8117        let options = Options::with_port(port).submit(Default::default());
8118        const NUM_NODES: usize = 2;
8119        // Initialize storage for each node
8120        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
8121
8122        let persistence_options: [_; NUM_NODES] = storage
8123            .iter()
8124            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
8125            .collect::<Vec<_>>()
8126            .try_into()
8127            .unwrap();
8128
8129        let network_config = TestConfigBuilder::default().build();
8130
8131        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
8132            .api_config(SqlDataSource::options(&storage[0], options))
8133            .network_config(network_config)
8134            .persistences(persistence_options.clone())
8135            .build();
8136        let network = TestNetwork::new(config, MOCK_SEQUENCER_VERSIONS).await;
8137        let mut events = network.server.event_stream();
8138        let mut all_transactions = HashMap::new();
8139        let mut namespace_tx: HashMap<_, HashSet<_>> = HashMap::new();
8140
8141        // Submit transactions to namespaces 1 through 4
8142
8143        for namespace in 1..=4 {
8144            for _count in 0..namespace {
8145                let payload_len = rng.gen_range(4..=10);
8146                let payload: Vec<u8> = (0..payload_len).map(|_| rng.r#gen()).collect();
8147
8148                let txn = Transaction::new(NamespaceId::from(namespace as u32), payload);
8149
8150                client.connect(None).await;
8151
8152                let hash = client
8153                    .post("submit/submit")
8154                    .body_json(&txn)
8155                    .unwrap()
8156                    .send()
8157                    .await
8158                    .unwrap();
8159                assert_eq!(txn.commit(), hash);
8160
8161                // Wait for a Decide event containing transaction matching the one we sent
8162                wait_for_decide_on_handle(&mut events, &txn).await;
8163                // Store transaction for later validation
8164
8165                all_transactions.insert(txn.commit(), txn.clone());
8166                namespace_tx.entry(namespace).or_default().insert(txn);
8167            }
8168        }
8169
8170        let mut transactions = client
8171            .socket("availability/stream/transactions/0")
8172            .subscribe::<TransactionQueryData<SeqTypes>>()
8173            .await
8174            .expect("failed to subscribe to transactions endpoint");
8175
8176        let mut count = 0;
8177        while let Some(tx) = transactions.next().await {
8178            let tx = tx.unwrap();
8179            let expected = all_transactions
8180                .get(&tx.transaction().commit())
8181                .expect("txn not found ");
8182            assert_eq!(tx.transaction(), expected, "invalid transaction");
8183            count += 1;
8184
8185            if count == all_transactions.len() {
8186                break;
8187            }
8188        }
8189
8190        // Validate namespace-specific stream endpoint
8191
8192        for (namespace, expected_ns_txns) in &namespace_tx {
8193            let mut api_namespace_txns = client
8194                .socket(&format!(
8195                    "availability/stream/transactions/0/namespace/{namespace}",
8196                ))
8197                .subscribe::<TransactionQueryData<SeqTypes>>()
8198                .await
8199                .unwrap_or_else(|_| {
8200                    panic!("failed to subscribe to transactions namespace {namespace}")
8201                });
8202
8203            let mut received = HashSet::new();
8204
8205            while let Some(res) = api_namespace_txns.next().await {
8206                let tx = res.expect("stream error");
8207                received.insert(tx.transaction().clone());
8208
8209                if received.len() == expected_ns_txns.len() {
8210                    break;
8211                }
8212            }
8213
8214            assert_eq!(
8215                received, *expected_ns_txns,
8216                "Mismatched transactions for namespace {namespace}"
8217            );
8218        }
8219    }
8220
8221    #[rstest]
8222    #[case(POS_V3)]
8223    #[case(POS_V4)]
8224    #[test_log::test(tokio::test(flavor = "multi_thread"))]
8225    async fn test_v3_and_v4_reward_tree_updates(#[case] upgrade: Upgrade) -> anyhow::Result<()> {
8226        // This test checks that the correct merkle tree is updated based on version
8227        //
8228        // When the protocol version is v3:
8229        // - The v3 Merkle tree is updated
8230        // - The v4 Merkle tree must be empty.
8231        //
8232        // When the protocol version is v4:
8233        // - The v4 Merkle tree is updated
8234        // - The v3 Merkle tree must be empty.
8235        const EPOCH_HEIGHT: u64 = 10;
8236
8237        let network_config = TestConfigBuilder::default()
8238            .epoch_height(EPOCH_HEIGHT)
8239            .build();
8240
8241        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
8242
8243        tracing::info!("API PORT = {api_port}");
8244        const NUM_NODES: usize = 5;
8245
8246        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
8247        let persistence: [_; NUM_NODES] = storage
8248            .iter()
8249            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
8250            .collect::<Vec<_>>()
8251            .try_into()
8252            .unwrap();
8253
8254        let config = TestNetworkConfigBuilder::with_num_nodes()
8255            .api_config(SqlDataSource::options(
8256                &storage[0],
8257                Options::with_port(api_port).catchup(Default::default()),
8258            ))
8259            .network_config(network_config)
8260            .persistences(persistence.clone())
8261            .catchups(std::array::from_fn(|_| {
8262                StatePeers::<StaticVersion<0, 1>>::from_urls(
8263                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
8264                    Default::default(),
8265                    Duration::from_secs(2),
8266                    &NoMetrics,
8267                )
8268            }))
8269            .pos_hook(
8270                DelegationConfig::MultipleDelegators,
8271                hotshot_contract_adapter::stake_table::StakeTableContractVersion::V3,
8272                upgrade,
8273            )
8274            .await
8275            .unwrap()
8276            .build();
8277        let mut network = TestNetwork::new(config, upgrade).await;
8278
8279        let mut events = network.peers[2].event_stream();
8280        // wait for 4 epochs
8281        wait_for_epochs(&mut events, EPOCH_HEIGHT, 4).await;
8282
8283        let validated_state = network.server.decided_state().await.unwrap();
8284        if upgrade.base == EPOCH_VERSION {
8285            let v1_tree = &validated_state.reward_merkle_tree_v1;
8286            assert!(v1_tree.num_leaves() > 0, "v1 reward tree tree is empty");
8287            let v2_tree = &validated_state.reward_merkle_tree_v2;
8288            assert!(
8289                v2_tree.num_leaves() == 0,
8290                "v2 reward tree tree is not empty"
8291            );
8292        } else {
8293            let v1_tree = &validated_state.reward_merkle_tree_v1;
8294            assert!(
8295                v1_tree.num_leaves() == 0,
8296                "v1 reward tree tree is not empty"
8297            );
8298            let v2_tree = &validated_state.reward_merkle_tree_v2;
8299            assert!(v2_tree.num_leaves() > 0, "v2 reward tree tree is empty");
8300        }
8301
8302        network.stop_consensus().await;
8303        Ok(())
8304    }
8305
8306    #[rstest]
8307    #[case(POS_V3)]
8308    #[case(POS_V4)]
8309    #[test_log::test(tokio::test(flavor = "multi_thread"))]
8310    pub(crate) async fn test_state_cert_query(#[case] upgrade: Upgrade) {
8311        const TEST_EPOCH_HEIGHT: u64 = 10;
8312        const TEST_EPOCHS: u64 = 5;
8313
8314        let network_config = TestConfigBuilder::default()
8315            .epoch_height(TEST_EPOCH_HEIGHT)
8316            .build();
8317
8318        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
8319
8320        tracing::info!("API PORT = {api_port}");
8321        const NUM_NODES: usize = 2;
8322
8323        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
8324        let persistence: [_; NUM_NODES] = storage
8325            .iter()
8326            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
8327            .collect::<Vec<_>>()
8328            .try_into()
8329            .unwrap();
8330
8331        let config = TestNetworkConfigBuilder::with_num_nodes()
8332            .api_config(SqlDataSource::options(
8333                &storage[0],
8334                Options::with_port(api_port).catchup(Default::default()),
8335            ))
8336            .network_config(network_config)
8337            .persistences(persistence.clone())
8338            .catchups(std::array::from_fn(|_| {
8339                StatePeers::<StaticVersion<0, 1>>::from_urls(
8340                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
8341                    Default::default(),
8342                    Duration::from_secs(2),
8343                    &NoMetrics,
8344                )
8345            }))
8346            .pos_hook(
8347                DelegationConfig::MultipleDelegators,
8348                hotshot_contract_adapter::stake_table::StakeTableContractVersion::V3,
8349                upgrade,
8350            )
8351            .await
8352            .unwrap()
8353            .build();
8354
8355        let network = TestNetwork::new(config, upgrade).await;
8356        let mut events = network.server.event_stream();
8357
8358        // Wait until 5 epochs have passed.
8359        loop {
8360            let event = events.next().await.unwrap();
8361            tracing::info!("Received event from handle: {event:?}");
8362
8363            if let CoordinatorEvent::LegacyEvent(Event {
8364                event: EventType::Decide { leaf_chain, .. },
8365                ..
8366            }) = event
8367            {
8368                println!(
8369                    "Decide event received: {:?}",
8370                    leaf_chain.first().unwrap().leaf.height()
8371                );
8372                if let Some(first_leaf) = leaf_chain.first() {
8373                    let height = first_leaf.leaf.height();
8374                    tracing::info!("Decide event received at height: {height}");
8375
8376                    if height >= TEST_EPOCHS * TEST_EPOCH_HEIGHT {
8377                        break;
8378                    }
8379                }
8380            }
8381        }
8382
8383        // Connect client.
8384        let client: Client<ClientErr, StaticVersion<0, 1>> =
8385            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
8386        client.connect(Some(Duration::from_secs(10))).await;
8387
8388        // Get the state cert for the epoch 3 to 5
8389        for i in 3..=TEST_EPOCHS {
8390            // v2
8391
8392            let state_query_data_v2 = client
8393                .get::<StateCertQueryDataV2<SeqTypes>>(&format!("availability/state-cert-v2/{i}"))
8394                .send()
8395                .await
8396                .unwrap();
8397            let state_cert_v2 = state_query_data_v2.0.clone();
8398            tracing::info!("state_cert_v2: {state_cert_v2:?}");
8399            assert_eq!(state_cert_v2.epoch.u64(), i);
8400            assert_eq!(
8401                state_cert_v2.light_client_state.block_height,
8402                i * TEST_EPOCH_HEIGHT - 5
8403            );
8404            let block_height = state_cert_v2.light_client_state.block_height;
8405
8406            let header: Header = client
8407                .get(&format!("availability/header/{block_height}"))
8408                .send()
8409                .await
8410                .unwrap();
8411
8412            // verify auth root if the consensus version is v4
8413            if header.version() == DRB_AND_HEADER_UPGRADE_VERSION {
8414                let auth_root = state_cert_v2.auth_root;
8415                let header_auth_root = header.auth_root().unwrap();
8416                if auth_root.is_zero() || header_auth_root.is_zero() {
8417                    panic!("auth root shouldn't be zero");
8418                }
8419
8420                assert_eq!(auth_root, header_auth_root, "auth root mismatch");
8421            }
8422
8423            // v1
8424            let state_query_data_v1 = client
8425                .get::<StateCertQueryDataV1<SeqTypes>>(&format!("availability/state-cert/{i}"))
8426                .send()
8427                .await
8428                .unwrap();
8429
8430            let state_cert_v1 = state_query_data_v1.0.clone();
8431            tracing::info!("state_cert_v1: {state_cert_v1:?}");
8432            assert_eq!(state_query_data_v1, state_query_data_v2.into());
8433        }
8434    }
8435
8436    /// Test state certificate catchup functionality by simulating a node that falls behind and needs
8437    /// to catch up. This test starts a 5-node network with epoch height 10, waits for 3 epochs to
8438    /// pass, then removes and restarts node 0 with a fresh storage. The
8439    /// restarted node catches up for the missing state certificates.
8440
8441    #[rstest]
8442    #[case(POS_V3)]
8443    #[case(POS_V4)]
8444    #[test_log::test(tokio::test(flavor = "multi_thread"))]
8445    pub(crate) async fn test_state_cert_catchup(#[case] upgrade: Upgrade) {
8446        const EPOCH_HEIGHT: u64 = 10;
8447
8448        let network_config = TestConfigBuilder::default()
8449            .epoch_height(EPOCH_HEIGHT)
8450            .build();
8451
8452        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
8453
8454        tracing::info!("API PORT = {api_port}");
8455        const NUM_NODES: usize = 5;
8456
8457        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
8458        let persistence: [_; NUM_NODES] = storage
8459            .iter()
8460            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
8461            .collect::<Vec<_>>()
8462            .try_into()
8463            .unwrap();
8464
8465        let config = TestNetworkConfigBuilder::with_num_nodes()
8466            .api_config(SqlDataSource::options(
8467                &storage[0],
8468                Options::with_port(api_port).light_client(Default::default()),
8469            ))
8470            .network_config(network_config)
8471            .persistences(persistence.clone())
8472            .catchups(std::array::from_fn(|_| {
8473                StatePeers::<StaticVersion<0, 1>>::from_urls(
8474                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
8475                    Default::default(),
8476                    Duration::from_secs(2),
8477                    &NoMetrics,
8478                )
8479            }))
8480            .pos_hook(
8481                DelegationConfig::MultipleDelegators,
8482                hotshot_contract_adapter::stake_table::StakeTableContractVersion::V3,
8483                upgrade,
8484            )
8485            .await
8486            .unwrap()
8487            .build();
8488        let state = config.states()[0].clone();
8489        let mut network = TestNetwork::new(config, upgrade).await;
8490
8491        let mut events = network.peers[2].event_stream();
8492        // Wait until at least 5 epochs have passed
8493        wait_for_epochs(&mut events, EPOCH_HEIGHT, 3).await;
8494
8495        // Remove peer 0 and restart it with the query module enabled.
8496        // Adding an additional node to the test network is not straight forward,
8497        // as the keys have already been initialized in the config above.
8498        // So, we remove this node and re-add it using the same index.
8499        network.peers.remove(0);
8500
8501        let new_storage: hotshot_query_service::data_source::sql::testing::TmpDb =
8502            SqlDataSource::create_storage().await;
8503        let new_persistence: persistence::sql::Options =
8504            <SqlDataSource as TestableSequencerDataSource>::persistence_options(&new_storage);
8505
8506        let node_0_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
8507        tracing::info!("node_0_port {node_0_port}");
8508        let opt = Options::with_port(node_0_port).query_sql(
8509            Query {
8510                peers: vec![format!("http://localhost:{api_port}").parse().unwrap()],
8511                ..Query::test()
8512            },
8513            tmp_options(&new_storage),
8514        );
8515        let node_0 = opt
8516            .clone()
8517            .serve(|metrics, consumer, storage| {
8518                let cfg = network.cfg.clone();
8519                let new_persistence = new_persistence.clone();
8520                let state = state.clone();
8521                async move {
8522                    Ok(cfg
8523                        .init_node(
8524                            1,
8525                            state,
8526                            new_persistence.clone(),
8527                            Some(StatePeers::<StaticVersion<0, 1>>::from_urls(
8528                                vec![format!("http://localhost:{api_port}").parse().unwrap()],
8529                                Default::default(),
8530                                Duration::from_secs(2),
8531                                &NoMetrics,
8532                            )),
8533                            storage,
8534                            &*metrics,
8535                            test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
8536                            consumer,
8537                            upgrade,
8538                            Default::default(),
8539                        )
8540                        .await)
8541                }
8542                .boxed()
8543            })
8544            .await
8545            .unwrap();
8546
8547        let mut events = node_0.event_stream();
8548        // Wait until at least 5 epochs have passed
8549        wait_for_epochs(&mut events, EPOCH_HEIGHT, 5).await;
8550
8551        let client: Client<ClientErr, StaticVersion<0, 1>> =
8552            Client::new(format!("http://localhost:{node_0_port}").parse().unwrap());
8553        client.connect(Some(Duration::from_secs(60))).await;
8554
8555        for epoch in 3..=5 {
8556            let state_cert = client
8557                .get::<StateCertQueryDataV2<SeqTypes>>(&format!(
8558                    "availability/state-cert-v2/{epoch}"
8559                ))
8560                .send()
8561                .await
8562                .unwrap();
8563            assert_eq!(state_cert.0.epoch.u64(), epoch);
8564        }
8565    }
8566
8567    #[test_log::test(tokio::test(flavor = "multi_thread"))]
8568    async fn test_integration_commission_updates() -> anyhow::Result<()> {
8569        const NUM_NODES: usize = 3;
8570        const EPOCH_HEIGHT: u64 = 10;
8571
8572        // Use version that supports epochs (V3 or V4)
8573        let versions = POS_V4;
8574
8575        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
8576
8577        // Initialize storage for nodes
8578        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
8579        let persistence: [_; NUM_NODES] = storage
8580            .iter()
8581            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
8582            .collect::<Vec<_>>()
8583            .try_into()
8584            .unwrap();
8585
8586        // Configure test network with epochs
8587        let network_config = TestConfigBuilder::default()
8588            .epoch_height(EPOCH_HEIGHT)
8589            .build();
8590
8591        // Build test network configuration starting with V1 stake table
8592        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
8593            .api_config(SqlDataSource::options(
8594                &storage[0],
8595                Options::with_port(api_port),
8596            ))
8597            .network_config(network_config.clone())
8598            .persistences(persistence.clone())
8599            .catchups(std::array::from_fn(|_| {
8600                StatePeers::<SequencerApiVersion>::from_urls(
8601                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
8602                    Default::default(),
8603                    Duration::from_secs(2),
8604                    &NoMetrics,
8605                )
8606            }))
8607            .pos_hook(
8608                // We want no new rewards after setting the commission to zero.
8609                DelegationConfig::NoSelfDelegation,
8610                StakeTableContractVersion::V1, // upgraded later
8611                POS_V4,
8612            )
8613            .await
8614            .unwrap()
8615            .build();
8616
8617        let network = TestNetwork::new(config, versions).await;
8618        let provider = network.cfg.anvil().unwrap();
8619        let deployer_addr = network.cfg.signer().address();
8620        let mut contracts = network.contracts.unwrap();
8621        let st_addr = contracts.address(Contract::StakeTableProxy).unwrap();
8622        upgrade_stake_table_v2(
8623            provider,
8624            L1Client::new(vec![network.cfg.l1_url()])?,
8625            &mut contracts,
8626            deployer_addr,
8627            deployer_addr,
8628        )
8629        .await?;
8630
8631        let mut commissions = vec![];
8632        for (i, (validator, provider)) in
8633            network_config.validator_providers().into_iter().enumerate()
8634        {
8635            let commission = fetch_commission(provider.clone(), st_addr, validator).await?;
8636            let new_commission = match i {
8637                0 => 0u16,
8638                1 => commission.to_evm() + 500u16,
8639                2 => commission.to_evm() - 100u16,
8640                _ => unreachable!(),
8641            }
8642            .try_into()?;
8643            commissions.push((validator, commission, new_commission));
8644            tracing::info!(%validator, %commission, %new_commission, "Update commission");
8645            update_commission(provider, st_addr, new_commission)
8646                .await?
8647                .get_receipt()
8648                .await?;
8649        }
8650
8651        // wait until new stake table takes effect
8652        let current_epoch = network.peers[0]
8653            .decided_leaf()
8654            .await
8655            .epoch(EPOCH_HEIGHT)
8656            .unwrap();
8657        let target_epoch = current_epoch.u64() + 3;
8658        println!("target epoch for new stake table: {target_epoch}");
8659        let mut events = network.peers[0].event_stream();
8660        wait_for_epochs(&mut events, EPOCH_HEIGHT, target_epoch).await;
8661
8662        // the last epoch with the old commissions
8663        let client: Client<ClientErr, SequencerApiVersion> =
8664            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
8665        let validators = client
8666            .get::<AuthenticatedValidatorMap>(&format!("node/validators/{}", target_epoch - 1))
8667            .send()
8668            .await
8669            .expect("validators");
8670        assert!(!validators.is_empty());
8671        for (val, old_comm, _) in commissions.clone() {
8672            assert_eq!(validators.get(&val).unwrap().commission, old_comm.to_evm());
8673        }
8674
8675        // the first epoch with the new commissions
8676        let client: Client<ClientErr, SequencerApiVersion> =
8677            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
8678        let validators = client
8679            .get::<AuthenticatedValidatorMap>(&format!("node/validators/{target_epoch}"))
8680            .send()
8681            .await
8682            .expect("validators");
8683        assert!(!validators.is_empty());
8684        for (val, _, new_comm) in commissions.clone() {
8685            assert_eq!(validators.get(&val).unwrap().commission, new_comm.to_evm());
8686        }
8687
8688        let last_block_with_old_commissions = EPOCH_HEIGHT * (target_epoch - 1);
8689        let block_with_new_commissions = EPOCH_HEIGHT * target_epoch;
8690        let mut new_amounts = vec![];
8691        for (val, ..) in commissions {
8692            let before = client
8693                .get::<Option<RewardAmount>>(&format!(
8694                    "reward-state-v2/reward-balance/{last_block_with_old_commissions}/{val}"
8695                ))
8696                .send()
8697                .await?
8698                .unwrap();
8699            let after = client
8700                .get::<Option<RewardAmount>>(&format!(
8701                    "reward-state-v2/reward-balance/{block_with_new_commissions}/{val}"
8702                ))
8703                .send()
8704                .await?
8705                .unwrap();
8706            new_amounts.push(after - before);
8707        }
8708
8709        let tolerance = U256::from(10 * EPOCH_HEIGHT).into();
8710        // validator zero got new new rewards except remainders
8711        assert!(new_amounts[0] < tolerance);
8712
8713        // other validators are still receiving rewards
8714        assert!(new_amounts[1] + new_amounts[2] > tolerance);
8715
8716        Ok(())
8717    }
8718
8719    /// Start on StakeTable V2, upgrade to V3, call `updateNetworkConfig` on one
8720    /// validator, and verify the indexer surfaces the new x25519 key and p2p
8721    /// address in the validator map.
8722    #[test_log::test(tokio::test(flavor = "multi_thread"))]
8723    async fn test_integration_update_fast_finality_network_config() -> anyhow::Result<()> {
8724        const NUM_NODES: usize = 3;
8725        const EPOCH_HEIGHT: u64 = 10;
8726
8727        let versions = POS_V4;
8728        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
8729
8730        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
8731        let persistence: [_; NUM_NODES] = storage
8732            .iter()
8733            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
8734            .collect::<Vec<_>>()
8735            .try_into()
8736            .unwrap();
8737
8738        let network_config = TestConfigBuilder::default()
8739            .epoch_height(EPOCH_HEIGHT)
8740            .build();
8741
8742        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
8743            .api_config(SqlDataSource::options(
8744                &storage[0],
8745                Options::with_port(api_port),
8746            ))
8747            .network_config(network_config.clone())
8748            .persistences(persistence.clone())
8749            .catchups(std::array::from_fn(|_| {
8750                StatePeers::<SequencerApiVersion>::from_urls(
8751                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
8752                    Default::default(),
8753                    Duration::from_secs(2),
8754                    &NoMetrics,
8755                )
8756            }))
8757            .pos_hook(
8758                DelegationConfig::MultipleDelegators,
8759                StakeTableContractVersion::V2,
8760                POS_V4,
8761            )
8762            .await
8763            .unwrap()
8764            .build();
8765
8766        let network = TestNetwork::new(config, versions).await;
8767        let provider = network.cfg.anvil().unwrap();
8768        let mut contracts = network.contracts.unwrap();
8769        let st_addr = contracts.address(Contract::StakeTableProxy).unwrap();
8770
8771        upgrade_stake_table_v3(provider, &mut contracts).await?;
8772
8773        let (validator, validator_provider) = network_config
8774            .validator_providers()
8775            .into_iter()
8776            .next()
8777            .unwrap();
8778        let x25519_key = x25519::Keypair::generate().unwrap().public_key();
8779        let p2p_addr: NetAddr = "127.0.0.1:9000".parse().unwrap();
8780        update_network_config(validator_provider, st_addr, x25519_key, p2p_addr.clone())
8781            .await?
8782            .get_receipt()
8783            .await?;
8784
8785        let current_epoch = network.peers[0]
8786            .decided_leaf()
8787            .await
8788            .epoch(EPOCH_HEIGHT)
8789            .unwrap();
8790        let target_epoch = current_epoch.u64() + 3;
8791        let mut events = network.peers[0].event_stream();
8792        wait_for_epochs(&mut events, EPOCH_HEIGHT, target_epoch).await;
8793
8794        let client: Client<ClientErr, SequencerApiVersion> =
8795            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
8796        let validators = client
8797            .get::<AuthenticatedValidatorMap>(&format!("node/validators/{target_epoch}"))
8798            .send()
8799            .await
8800            .expect("validators");
8801        let v = validators.get(&validator).expect("validator present");
8802        assert_eq!(v.x25519_key, Some(x25519_key));
8803        assert_eq!(v.p2p_addr, Some(p2p_addr));
8804
8805        Ok(())
8806    }
8807
8808    /// Assert the endpoint returns a 2xx status and a valid JSON body.
8809    async fn assert_json_endpoint(
8810        http: &reqwest::Client,
8811        api_port: u16,
8812        path: &str,
8813    ) -> anyhow::Result<()> {
8814        let resp = http
8815            .get(format!("http://localhost:{api_port}/v1/{path}"))
8816            .send()
8817            .await?;
8818        let status = resp.status();
8819        assert!(
8820            status.is_success(),
8821            "v1/{path}: returned {status}, expected 2xx"
8822        );
8823        resp.json::<serde_json::Value>().await?;
8824        Ok(())
8825    }
8826
8827    /// Assert the endpoint returns a well-formed JSON body, without constraining the status.
8828    /// For routes where an error response is the expected outcome but its exact status is not
8829    /// part of the contract.
8830    async fn assert_json_body(
8831        http: &reqwest::Client,
8832        api_port: u16,
8833        path: &str,
8834    ) -> anyhow::Result<()> {
8835        http.get(format!("http://localhost:{api_port}/v1/{path}"))
8836            .send()
8837            .await?
8838            .json::<serde_json::Value>()
8839            .await?;
8840        Ok(())
8841    }
8842
8843    /// Assert the endpoint returns the expected HTTP status code.
8844    async fn assert_endpoint_status(
8845        http: &reqwest::Client,
8846        api_port: u16,
8847        path: &str,
8848        expected_status: u16,
8849    ) -> anyhow::Result<()> {
8850        let status = http
8851            .get(format!("http://localhost:{api_port}/v1/{path}"))
8852            .send()
8853            .await?
8854            .status()
8855            .as_u16();
8856        assert_eq!(
8857            status, expected_status,
8858            "v1/{path}: should return {expected_status}, got {status}"
8859        );
8860        Ok(())
8861    }
8862
8863    /// Assert the endpoint returns a 2xx status, without requiring a JSON body. Used for
8864    /// endpoints whose content is not JSON or varies between calls (e.g. live metrics).
8865    async fn assert_endpoint_ok(
8866        http: &reqwest::Client,
8867        api_port: u16,
8868        path: &str,
8869    ) -> anyhow::Result<()> {
8870        let status = http
8871            .get(format!("http://localhost:{api_port}/v1/{path}"))
8872            .send()
8873            .await?
8874            .status();
8875        assert!(
8876            status.is_success(),
8877            "v1/{path}: returned {status}, expected 2xx"
8878        );
8879        Ok(())
8880    }
8881
8882    /// Assert an endpoint that fails via `ApiError` returns the expected status and the
8883    /// `{"Custom":{"message","status"}}` error envelope that existing clients parse.
8884    async fn assert_error_body(
8885        http: &reqwest::Client,
8886        api_port: u16,
8887        path: &str,
8888        expected_status: u16,
8889    ) -> anyhow::Result<()> {
8890        let resp = http
8891            .get(format!("http://localhost:{api_port}/v1/{path}"))
8892            .send()
8893            .await?;
8894        let status = resp.status().as_u16();
8895        let body: serde_json::Value = resp.json().await?;
8896        assert_eq!(status, expected_status, "v1/{path}: status");
8897        let custom = body
8898            .get("Custom")
8899            .unwrap_or_else(|| panic!("v1/{path}: error body missing Custom envelope: {body}"));
8900        assert_eq!(
8901            custom.get("status").and_then(|s| s.as_u64()),
8902            Some(u64::from(expected_status)),
8903            "v1/{path}: envelope status: {body}"
8904        );
8905        assert!(
8906            custom.get("message").is_some_and(|m| m.is_string()),
8907            "v1/{path}: envelope message missing: {body}"
8908        );
8909        Ok(())
8910    }
8911
8912    /// POST a VBS-binary body and assert the server accepts it.
8913    ///
8914    /// VBS (Versioned Binary Serialization) is what production peer-catchup and
8915    /// `submit-transactions` clients use via `http_client::Request::body_binary`. This helper
8916    /// catches regressions where the handler accepts only JSON.
8917    async fn assert_post_binary<B: serde::Serialize>(
8918        http: &reqwest::Client,
8919        api_port: u16,
8920        path: &str,
8921        body: &B,
8922    ) -> anyhow::Result<()> {
8923        use vbs::{BinarySerializer, Serializer, version::StaticVersion};
8924        let payload = Serializer::<StaticVersion<0, 1>>::serialize(body)?;
8925        let resp = http
8926            .post(format!("http://localhost:{api_port}/v1/{path}"))
8927            .header("Content-Type", "application/octet-stream")
8928            .header("Accept", "application/octet-stream")
8929            .body(payload)
8930            .send()
8931            .await?;
8932        let status = resp.status();
8933        assert!(
8934            status.is_success(),
8935            "v1/{path}: binary POST returned {status}, expected 2xx"
8936        );
8937        Ok(())
8938    }
8939
8940    /// Connect to the WebSocket endpoint, collect up to 10 messages, and assert that at least 2
8941    /// of them are valid JSON.
8942    async fn assert_ws_endpoint(api_port: u16, path: &str) -> anyhow::Result<()> {
8943        use std::time::Duration;
8944
8945        use futures::StreamExt as _;
8946        use tokio::time::timeout;
8947        use tokio_tungstenite::{connect_async, tungstenite::Message};
8948
8949        let url = format!("ws://localhost:{api_port}/v1/{path}");
8950        let (mut ws, _) = connect_async(&url).await?;
8951        let mut messages = Vec::new();
8952        while messages.len() < 10 {
8953            match timeout(Duration::from_millis(500), ws.next()).await {
8954                Ok(Some(Ok(Message::Text(text)))) => {
8955                    if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
8956                        messages.push(v);
8957                    }
8958                },
8959                _ => break,
8960            }
8961        }
8962
8963        assert!(
8964            messages.len() >= 2,
8965            "v1/{path}: expected >=2 JSON messages from the stream, got {}",
8966            messages.len(),
8967        );
8968        Ok(())
8969    }
8970
8971    /// Same as `assert_ws_endpoint` but exercises the binary (`Accept: application/octet-stream`)
8972    /// path that our clients use by default. Asserts the server sends `Message::Binary` frames
8973    /// carrying VBS-encoded payloads.
8974    async fn assert_ws_endpoint_binary(api_port: u16, path: &str) -> anyhow::Result<()> {
8975        use std::time::Duration;
8976
8977        use futures::StreamExt as _;
8978        use tokio::time::timeout;
8979        use tokio_tungstenite::{
8980            connect_async,
8981            tungstenite::{client::IntoClientRequest, http::HeaderValue, protocol::Message},
8982        };
8983
8984        let url = format!("ws://localhost:{api_port}/v1/{path}");
8985        let mut req = url.as_str().into_client_request()?;
8986        req.headers_mut().insert(
8987            "Accept",
8988            HeaderValue::from_static("application/octet-stream"),
8989        );
8990        let (mut ws, _) = connect_async(req).await?;
8991        let mut frames = Vec::new();
8992        while frames.len() < 3 {
8993            match timeout(Duration::from_millis(500), ws.next()).await {
8994                Ok(Some(Ok(Message::Binary(bytes)))) => frames.push(bytes.to_vec()),
8995                _ => break,
8996            }
8997        }
8998
8999        assert!(
9000            !frames.is_empty(),
9001            "v1/{path}: no binary frames (Accept: application/octet-stream); handler likely \
9002             always sends text",
9003        );
9004        Ok(())
9005    }
9006
9007    #[rstest]
9008    #[case(POS_V4)]
9009    #[test_log::test]
9010    fn test_reward_proof_endpoint(#[case] upgrade: Upgrade) {
9011        let test = async move {
9012            const EPOCH_HEIGHT: u64 = 10;
9013            const NUM_NODES: usize = 5;
9014
9015            let network_config = TestConfigBuilder::default()
9016                .epoch_height(EPOCH_HEIGHT)
9017                .build();
9018
9019            let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
9020            println!("API PORT = {api_port}");
9021
9022            let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
9023            let persistence: [_; NUM_NODES] = storage
9024                .iter()
9025                .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
9026                .collect::<Vec<_>>()
9027                .try_into()
9028                .unwrap();
9029
9030            let api_opts = Options::with_port(api_port)
9031                .catchup(Default::default())
9032                .config(Default::default())
9033                .explorer(Default::default())
9034                .light_client(Default::default())
9035                .hotshot_events(Default::default());
9036
9037            let config = TestNetworkConfigBuilder::with_num_nodes()
9038                .api_config(SqlDataSource::options(&storage[0], api_opts))
9039                .network_config(network_config.clone())
9040                .persistences(persistence.clone())
9041                .catchups(std::array::from_fn(|_| {
9042                    StatePeers::<StaticVersion<0, 1>>::from_urls(
9043                        vec![format!("http://localhost:{api_port}").parse().unwrap()],
9044                        Default::default(),
9045                        Duration::from_secs(2),
9046                        &NoMetrics,
9047                    )
9048                }))
9049                .pos_hook(
9050                    DelegationConfig::MultipleDelegators,
9051                    hotshot_contract_adapter::stake_table::StakeTableContractVersion::V3,
9052                    upgrade,
9053                )
9054                .await
9055                .unwrap()
9056                .build();
9057
9058            let mut network = TestNetwork::new(config, upgrade).await;
9059
9060            // wait for 4 epochs
9061            let mut events = network.server.event_stream();
9062            wait_for_epochs(&mut events, EPOCH_HEIGHT, 4).await;
9063
9064            let url = format!("http://localhost:{api_port}").parse().unwrap();
9065            let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
9066
9067            let validated_state = network.server.decided_state().await.unwrap();
9068            let decided_leaf = network.server.decided_leaf().await;
9069            let height = decided_leaf.height();
9070
9071            // validate proof returned from the api
9072            if upgrade.base == EPOCH_VERSION {
9073                // V1 case: only the legacy v1 reward tree endpoints apply here
9074                wait_until_block_height(&client, "reward-state/block-height", height).await;
9075
9076                network.stop_consensus().await;
9077
9078                for (address, _) in validated_state.reward_merkle_tree_v1.iter() {
9079                    let (_, expected_proof) = validated_state
9080                        .reward_merkle_tree_v1
9081                        .lookup(*address)
9082                        .expect_ok()
9083                        .unwrap();
9084
9085                    let res = client
9086                        .get::<RewardAccountQueryDataV1>(&format!(
9087                            "reward-state/proof/{height}/{address}"
9088                        ))
9089                        .send()
9090                        .await
9091                        .unwrap();
9092
9093                    match res.proof.proof {
9094                        RewardMerkleProofV1::Presence(p) => {
9095                            assert_eq!(
9096                                p, expected_proof,
9097                                "Proof mismatch for V1 at {height}, addr={address}"
9098                            );
9099                        },
9100                        other => panic!(
9101                            "Expected Present proof for V1 at {height}, addr={address}, got \
9102                             {other:?}"
9103                        ),
9104                    }
9105                }
9106            } else {
9107                // V2 case
9108
9109                // Submit two transactions to the same namespace in separate blocks
9110                // so the namespace-filtered WS stream produces ≥2 messages.
9111                // Submitting both at once risks the builder batching them into a
9112                // single block; submitting sequentially (wait between) guarantees
9113                // different blocks so the second wait_for_decide_on_handle doesn't
9114                // hang looking for an event that was already consumed by the first.
9115                let avail_ns = NamespaceId::from(42_u32);
9116                let avail_tx = Transaction::new(avail_ns, vec![1, 2, 3]);
9117                network
9118                    .server
9119                    .submit_transaction(avail_tx.clone())
9120                    .await
9121                    .unwrap();
9122                let (avail_block, _) = wait_for_decide_on_handle(&mut events, &avail_tx).await;
9123
9124                // Submit the second transaction only after the first is decided,
9125                // ensuring it lands in a strictly later block.
9126                let avail_tx2 = Transaction::new(avail_ns, vec![4, 5, 6]);
9127                network
9128                    .server
9129                    .submit_transaction(avail_tx2.clone())
9130                    .await
9131                    .unwrap();
9132                wait_for_decide_on_handle(&mut events, &avail_tx2).await;
9133
9134                wait_until_block_height(&client, "reward-state-v2/block-height", height).await;
9135                // Wait for the availability query service to index avail_block.
9136                wait_until_block_height(&client, "node/block-height", avail_block).await;
9137
9138                // Sample a fee account for the fee-state comparisons below.
9139                // `validated_state` was captured before the fee-paying blocks above were
9140                // decided, so its fee tree can still be empty; poll the decided state while
9141                // consensus is still running (it is frozen after stop_consensus). The
9142                // decided state can also contain accounts added after `avail_block`, so
9143                // only accept an account provable at the `avail_block` snapshot queried
9144                // in the comparisons.
9145                let sample_start = Instant::now();
9146                let fee_account = 'fee_account: loop {
9147                    let state = network.server.decided_state().await.unwrap();
9148                    for (addr, _) in state.fee_merkle_tree.iter() {
9149                        if client
9150                            .get::<MerkleProof<FeeAmount, FeeAccount, Sha3Node, 256>>(&format!(
9151                                "fee-state/{avail_block}/{addr}"
9152                            ))
9153                            .send()
9154                            .await
9155                            .is_ok()
9156                        {
9157                            break 'fee_account *addr;
9158                        }
9159                    }
9160                    assert!(
9161                        sample_start.elapsed() < Duration::from_secs(30),
9162                        "no fee account provable at avail_block {avail_block} after 30s"
9163                    );
9164                    sleep(Duration::from_millis(500)).await;
9165                };
9166
9167                network.stop_consensus().await;
9168
9169                let http = reqwest::Client::new();
9170
9171                for (address, _) in validated_state.reward_merkle_tree_v2.iter() {
9172                    let (_, expected_proof) = validated_state
9173                        .reward_merkle_tree_v2
9174                        .lookup(*address)
9175                        .expect_ok()
9176                        .unwrap();
9177
9178                    let res = client
9179                        .get::<RewardAccountQueryDataV2>(&format!(
9180                            "reward-state-v2/proof/{height}/{address}"
9181                        ))
9182                        .send()
9183                        .await
9184                        .unwrap();
9185
9186                    match res.proof.proof.clone() {
9187                        RewardMerkleProofV2::Presence(p) => {
9188                            assert_eq!(
9189                                p, expected_proof,
9190                                "Proof mismatch for V2 at {height}, addr={address}"
9191                            );
9192                        },
9193                        other => panic!(
9194                            "Expected Present proof for V2 at {height}, addr={address}, got \
9195                             {other:?}"
9196                        ),
9197                    }
9198
9199                    let reward_claim_input = client
9200                        .get::<RewardClaimInput>(&format!(
9201                            "reward-state-v2/reward-claim-input/{height}/{address}"
9202                        ))
9203                        .send()
9204                        .await
9205                        .unwrap();
9206
9207                    assert_eq!(reward_claim_input, res.to_reward_claim_input()?);
9208
9209                    // Behavior relied on by scripts/claim-rewards-loop: an account with no
9210                    // rewards yields 404; any other error status makes the claim loop exit and
9211                    // process-compose tear down the whole demo.
9212                    let absent = alloy::primitives::Address::with_last_byte(0xaa);
9213                    assert!(
9214                        validated_state
9215                            .reward_merkle_tree_v2
9216                            .iter()
9217                            .all(|(addr, _)| addr.0 != absent),
9218                        "sentinel address unexpectedly present in reward tree"
9219                    );
9220                    let err = client
9221                        .get::<RewardClaimInput>(&format!(
9222                            "reward-state-v2/reward-claim-input/{height}/{absent}"
9223                        ))
9224                        .send()
9225                        .await
9226                        .unwrap_err();
9227                    assert_matches!(err, ClientErr { status, .. } if status == StatusCode::NOT_FOUND);
9228
9229                    // Smoke-check each per-address endpoint under reward-state-v2.
9230                    assert_json_endpoint(
9231                        &http,
9232                        api_port,
9233                        &format!("reward-state-v2/proof/{height}/{address}"),
9234                    )
9235                    .await?;
9236                    assert_json_endpoint(
9237                        &http,
9238                        api_port,
9239                        &format!("reward-state-v2/reward-claim-input/{height}/{address}"),
9240                    )
9241                    .await?;
9242                    assert_json_endpoint(
9243                        &http,
9244                        api_port,
9245                        &format!("reward-state-v2/reward-balance/{height}/{address}"),
9246                    )
9247                    .await?;
9248                    assert_json_endpoint(
9249                        &http,
9250                        api_port,
9251                        &format!("reward-state-v2/proof/latest/{address}"),
9252                    )
9253                    .await?;
9254                    assert_json_endpoint(
9255                        &http,
9256                        api_port,
9257                        &format!("reward-state-v2/reward-balance/latest/{address}"),
9258                    )
9259                    .await?;
9260
9261                    // The reward-state mount shares its handlers with reward-state-v2 for
9262                    // backwards compatibility, so these two routes hit the same v2-tree-backed
9263                    // handlers as the pair above, just under reward-state.
9264                    assert_json_endpoint(
9265                        &http,
9266                        api_port,
9267                        &format!("reward-state/proof/latest/{address}"),
9268                    )
9269                    .await?;
9270                    assert_json_endpoint(
9271                        &http,
9272                        api_port,
9273                        &format!("reward-state/reward-balance/latest/{address}"),
9274                    )
9275                    .await?;
9276                }
9277
9278                assert_json_endpoint(
9279                    &http,
9280                    api_port,
9281                    &format!("reward-state-v2/reward-amounts/{height}/0/1000"),
9282                )
9283                .await?;
9284                assert_json_endpoint(
9285                    &http,
9286                    api_port,
9287                    &format!("reward-state-v2/reward-merkle-tree-v2/{height}"),
9288                )
9289                .await?;
9290                assert_json_endpoint(
9291                    &http,
9292                    api_port,
9293                    &format!("reward-state/reward-amounts/{height}/0/1000"),
9294                )
9295                .await?;
9296                assert_json_endpoint(
9297                    &http,
9298                    api_port,
9299                    &format!("reward-state/reward-merkle-tree-v2/{height}"),
9300                )
9301                .await?;
9302
9303                // Merklized-state `get_path` routes, inherited by both reward mounts from
9304                // the legacy `hotshot-query-service` merklized-state base routes (mirrors the block-state /
9305                // fee-state checks below). Nothing in this codebase populates the generic
9306                // merklized-state tables for the reward trees today; the reward-state modules
9307                // persist snapshots via the separate `persist_tree`/`load_tree` bincode-blob
9308                // mechanism instead, so these routes fail in practice. We only assert that both
9309                // mounts, in both height and commit form, return well-formed JSON.
9310                let reward_address = validated_state
9311                    .reward_merkle_tree_v2
9312                    .iter()
9313                    .next()
9314                    .map(|(addr, _)| *addr)
9315                    .expect("reward tree should have at least one account");
9316                let reward_header: Header = client
9317                    .get(&format!("availability/header/{height}"))
9318                    .send()
9319                    .await
9320                    .unwrap();
9321                let reward_mt_commit = match reward_header.reward_merkle_tree_root() {
9322                    either::Either::Left(commit) => commit.to_string(),
9323                    either::Either::Right(commit) => commit.to_string(),
9324                };
9325                for mount in ["reward-state", "reward-state-v2"] {
9326                    assert_json_body(
9327                        &http,
9328                        api_port,
9329                        &format!("{mount}/{height}/{reward_address}"),
9330                    )
9331                    .await?;
9332                    assert_json_body(
9333                        &http,
9334                        api_port,
9335                        &format!("{mount}/commit/{reward_mt_commit}/{reward_address}"),
9336                    )
9337                    .await?;
9338                }
9339
9340                // Availability v1 routes.
9341
9342                // Namespace proof by height
9343                assert_json_endpoint(
9344                    &http,
9345                    api_port,
9346                    &format!("availability/block/{avail_block}/namespace/{avail_ns}"),
9347                )
9348                .await?;
9349
9350                // Namespace proof by block hash and payload hash
9351                let avail_header: Header = client
9352                    .get(&format!("availability/header/{avail_block}"))
9353                    .send()
9354                    .await
9355                    .unwrap();
9356                assert_json_endpoint(
9357                    &http,
9358                    api_port,
9359                    &format!(
9360                        "availability/block/hash/{}/namespace/{avail_ns}",
9361                        avail_header.commit()
9362                    ),
9363                )
9364                .await?;
9365                assert_json_endpoint(
9366                    &http,
9367                    api_port,
9368                    &format!(
9369                        "availability/block/payload-hash/{}/namespace/{avail_ns}",
9370                        avail_header.payload_commitment()
9371                    ),
9372                )
9373                .await?;
9374
9375                // Namespace proof range
9376                assert_json_endpoint(
9377                    &http,
9378                    api_port,
9379                    &format!(
9380                        "availability/block/{avail_block}/{}/namespace/{avail_ns}",
9381                        avail_block + 1
9382                    ),
9383                )
9384                .await?;
9385
9386                // State certificate endpoints (epoch 1 is complete after 4 epochs)
9387                assert_json_endpoint(&http, api_port, "availability/state-cert/1").await?;
9388                assert_json_endpoint(&http, api_port, "availability/state-cert-v2/1").await?;
9389
9390                // HotShot availability endpoints: leaf, header, block, payload, vid/common, etc.
9391                let avail_leaf: LeafQueryData<SeqTypes> = client
9392                    .get(&format!("availability/leaf/{avail_block}"))
9393                    .send()
9394                    .await
9395                    .unwrap();
9396                let leaf_hash = avail_leaf.hash();
9397                let block_hash = avail_header.commit();
9398                let payload_hash = avail_header.payload_commitment();
9399
9400                // Leaf endpoints
9401                assert_json_endpoint(&http, api_port, &format!("availability/leaf/{avail_block}"))
9402                    .await?;
9403                assert_json_endpoint(
9404                    &http,
9405                    api_port,
9406                    &format!("availability/leaf/hash/{leaf_hash}"),
9407                )
9408                .await?;
9409                assert_json_endpoint(
9410                    &http,
9411                    api_port,
9412                    &format!("availability/leaf/{avail_block}/{}", avail_block + 1),
9413                )
9414                .await?;
9415
9416                // Header endpoints
9417                assert_json_endpoint(
9418                    &http,
9419                    api_port,
9420                    &format!("availability/header/{avail_block}"),
9421                )
9422                .await?;
9423                assert_json_endpoint(
9424                    &http,
9425                    api_port,
9426                    &format!("availability/header/hash/{block_hash}"),
9427                )
9428                .await?;
9429                assert_json_endpoint(
9430                    &http,
9431                    api_port,
9432                    &format!("availability/header/payload-hash/{payload_hash}"),
9433                )
9434                .await?;
9435                assert_json_endpoint(
9436                    &http,
9437                    api_port,
9438                    &format!("availability/header/{avail_block}/{}", avail_block + 1),
9439                )
9440                .await?;
9441
9442                // Block endpoints
9443                assert_json_endpoint(
9444                    &http,
9445                    api_port,
9446                    &format!("availability/block/{avail_block}"),
9447                )
9448                .await?;
9449                assert_json_endpoint(
9450                    &http,
9451                    api_port,
9452                    &format!("availability/block/hash/{block_hash}"),
9453                )
9454                .await?;
9455                assert_json_endpoint(
9456                    &http,
9457                    api_port,
9458                    &format!("availability/block/payload-hash/{payload_hash}"),
9459                )
9460                .await?;
9461                assert_json_endpoint(
9462                    &http,
9463                    api_port,
9464                    &format!("availability/block/{avail_block}/{}", avail_block + 1),
9465                )
9466                .await?;
9467
9468                // Payload endpoints
9469                assert_json_endpoint(
9470                    &http,
9471                    api_port,
9472                    &format!("availability/payload/{avail_block}"),
9473                )
9474                .await?;
9475                assert_json_endpoint(
9476                    &http,
9477                    api_port,
9478                    &format!("availability/payload/hash/{payload_hash}"),
9479                )
9480                .await?;
9481                assert_json_endpoint(
9482                    &http,
9483                    api_port,
9484                    &format!("availability/payload/block-hash/{block_hash}"),
9485                )
9486                .await?;
9487                assert_json_endpoint(
9488                    &http,
9489                    api_port,
9490                    &format!("availability/payload/{avail_block}/{}", avail_block + 1),
9491                )
9492                .await?;
9493
9494                // VID common endpoints
9495                assert_json_endpoint(
9496                    &http,
9497                    api_port,
9498                    &format!("availability/vid/common/{avail_block}"),
9499                )
9500                .await?;
9501                assert_json_endpoint(
9502                    &http,
9503                    api_port,
9504                    &format!("availability/vid/common/hash/{block_hash}"),
9505                )
9506                .await?;
9507                assert_json_endpoint(
9508                    &http,
9509                    api_port,
9510                    &format!("availability/vid/common/payload-hash/{payload_hash}"),
9511                )
9512                .await?;
9513                assert_json_endpoint(
9514                    &http,
9515                    api_port,
9516                    &format!("availability/vid/common/{avail_block}/{}", avail_block + 1),
9517                )
9518                .await?;
9519
9520                // Transaction endpoints
9521                let tx_hash = avail_tx.commit();
9522                assert_json_endpoint(
9523                    &http,
9524                    api_port,
9525                    &format!("availability/transaction/{avail_block}/0/noproof"),
9526                )
9527                .await?;
9528                assert_json_endpoint(
9529                    &http,
9530                    api_port,
9531                    &format!("availability/transaction/hash/{tx_hash}/noproof"),
9532                )
9533                .await?;
9534                assert_json_endpoint(
9535                    &http,
9536                    api_port,
9537                    &format!("availability/transaction/{avail_block}/0/proof"),
9538                )
9539                .await?;
9540                assert_json_endpoint(
9541                    &http,
9542                    api_port,
9543                    &format!("availability/transaction/hash/{tx_hash}/proof"),
9544                )
9545                .await?;
9546                assert_json_endpoint(
9547                    &http,
9548                    api_port,
9549                    &format!("availability/transaction/{avail_block}/0"),
9550                )
9551                .await?;
9552                assert_json_endpoint(
9553                    &http,
9554                    api_port,
9555                    &format!("availability/transaction/hash/{tx_hash}"),
9556                )
9557                .await?;
9558
9559                // Block summary endpoints
9560                assert_json_endpoint(
9561                    &http,
9562                    api_port,
9563                    &format!("availability/block/summary/{avail_block}"),
9564                )
9565                .await?;
9566                assert_json_endpoint(
9567                    &http,
9568                    api_port,
9569                    &format!(
9570                        "availability/block/summaries/{avail_block}/{}",
9571                        avail_block + 1
9572                    ),
9573                )
9574                .await?;
9575
9576                // Limits endpoint (static response)
9577                assert_json_endpoint(&http, api_port, "availability/limits").await?;
9578
9579                // Cert2 endpoint: `avail_block` is a mid-chain block with no cert2, so both APIs
9580                // return 404. Compare status only, since the two error bodies differ by design.
9581                assert_endpoint_status(
9582                    &http,
9583                    api_port,
9584                    &format!("availability/cert2/{avail_block}"),
9585                    404,
9586                )
9587                .await?;
9588
9589                // WebSocket streaming endpoints.
9590                //
9591                // For unfiltered streams, start 10 blocks before avail_block so there are at
9592                // least 10 committed blocks ready to stream (consensus has already stopped).
9593                // For namespace-filtered streams, start at avail_block where the two submitted
9594                // transactions were included, giving >=2 matching messages.
9595                let ws_start = avail_block.saturating_sub(10);
9596                assert_ws_endpoint(api_port, &format!("availability/stream/leaves/{ws_start}"))
9597                    .await?;
9598                assert_ws_endpoint(api_port, &format!("availability/stream/headers/{ws_start}"))
9599                    .await?;
9600                assert_ws_endpoint(api_port, &format!("availability/stream/blocks/{ws_start}"))
9601                    .await?;
9602                assert_ws_endpoint(
9603                    api_port,
9604                    &format!("availability/stream/payloads/{ws_start}"),
9605                )
9606                .await?;
9607                assert_ws_endpoint(
9608                    api_port,
9609                    &format!("availability/stream/vid/common/{ws_start}"),
9610                )
9611                .await?;
9612                assert_ws_endpoint(
9613                    api_port,
9614                    &format!("availability/stream/transactions/{ws_start}"),
9615                )
9616                .await?;
9617                // Namespace-filtered streams: start at avail_block; two transactions were
9618                // submitted so the stream produces ≥2 messages.
9619                assert_ws_endpoint(
9620                    api_port,
9621                    &format!("availability/stream/transactions/{avail_block}/namespace/{avail_ns}"),
9622                )
9623                .await?;
9624                assert_ws_endpoint(
9625                    api_port,
9626                    &format!("availability/stream/blocks/{avail_block}/namespace/{avail_ns}"),
9627                )
9628                .await?;
9629
9630                // Our clients default to `Accept: application/octet-stream`, so the server must
9631                // emit `Message::Binary` (VBS-encoded) frames on that path. Verify it does so
9632                // on a representative stream.
9633                assert_ws_endpoint_binary(
9634                    api_port,
9635                    &format!("availability/stream/leaves/{ws_start}"),
9636                )
9637                .await?;
9638
9639                // Merklized state endpoints (block-state and fee-state). Wait for
9640                // the backend to have indexed the snapshot we'll query.
9641                wait_until_block_height(&client, "block-state/block-height", avail_block).await;
9642                wait_until_block_height(&client, "fee-state/block-height", avail_block).await;
9643
9644                // block-state/block-height and fee-state/block-height (latest
9645                // height for which merklized state is available).
9646                assert_json_endpoint(&http, api_port, "block-state/block-height").await?;
9647                assert_json_endpoint(&http, api_port, "fee-state/block-height").await?;
9648
9649                // block-state path by height: the merkle tree at height H
9650                // contains the headers of blocks [0, H), so a valid key is H-1.
9651                assert_json_endpoint(
9652                    &http,
9653                    api_port,
9654                    &format!(
9655                        "block-state/{avail_block}/{}",
9656                        avail_block.saturating_sub(1)
9657                    ),
9658                )
9659                .await?;
9660
9661                // block-state path by commit. Use the tree commitment from
9662                // the header at avail_block.
9663                let block_mt_commit = avail_header.block_merkle_tree_root().to_string();
9664                assert_json_endpoint(
9665                    &http,
9666                    api_port,
9667                    &format!(
9668                        "block-state/commit/{block_mt_commit}/{}",
9669                        avail_block.saturating_sub(1)
9670                    ),
9671                )
9672                .await?;
9673
9674                // fee-state path by height for a known fee account (sampled above while
9675                // consensus was running), and fee-balance/latest for the same account.
9676                assert_json_endpoint(
9677                    &http,
9678                    api_port,
9679                    &format!("fee-state/{avail_block}/{fee_account}"),
9680                )
9681                .await?;
9682                let fee_mt_commit = avail_header.fee_merkle_tree_root().to_string();
9683                assert_json_endpoint(
9684                    &http,
9685                    api_port,
9686                    &format!("fee-state/commit/{fee_mt_commit}/{fee_account}"),
9687                )
9688                .await?;
9689                assert_json_endpoint(
9690                    &http,
9691                    api_port,
9692                    &format!("fee-state/fee-balance/latest/{fee_account}"),
9693                )
9694                .await?;
9695
9696                // Status endpoints. Block height and success rate are stable since consensus is
9697                // stopped; time-since-last-decide and metrics vary by wall-clock so we only
9698                // check for a 2xx.
9699                assert_json_endpoint(&http, api_port, "status/block-height").await?;
9700                assert_json_endpoint(&http, api_port, "status/success-rate").await?;
9701                assert_endpoint_ok(&http, api_port, "status/time-since-last-decide").await?;
9702                assert_endpoint_ok(&http, api_port, "status/metrics").await?;
9703
9704                // Config endpoints. /runtime returns 404 because no PublicNodeConfig was
9705                // configured for this test.
9706                assert_json_endpoint(&http, api_port, "config/hotshot").await?;
9707                assert_json_endpoint(&http, api_port, "config/env").await?;
9708                assert_endpoint_status(&http, api_port, "config/runtime", 404).await?;
9709
9710                // Node endpoints.
9711                assert_json_endpoint(&http, api_port, "node/block-height").await?;
9712                assert_json_endpoint(&http, api_port, "node/transactions/count").await?;
9713                assert_json_endpoint(
9714                    &http,
9715                    api_port,
9716                    &format!("node/transactions/count/{avail_block}"),
9717                )
9718                .await?;
9719                assert_json_endpoint(
9720                    &http,
9721                    api_port,
9722                    &format!("node/transactions/count/0/{avail_block}"),
9723                )
9724                .await?;
9725                assert_json_endpoint(
9726                    &http,
9727                    api_port,
9728                    &format!("node/transactions/count/namespace/{avail_ns}"),
9729                )
9730                .await?;
9731                assert_json_endpoint(
9732                    &http,
9733                    api_port,
9734                    &format!("node/transactions/count/namespace/{avail_ns}/{avail_block}"),
9735                )
9736                .await?;
9737                assert_json_endpoint(
9738                    &http,
9739                    api_port,
9740                    &format!("node/transactions/count/namespace/{avail_ns}/0/{avail_block}"),
9741                )
9742                .await?;
9743
9744                assert_json_endpoint(&http, api_port, "node/payloads/size").await?;
9745                assert_json_endpoint(&http, api_port, "node/payloads/total-size").await?;
9746                assert_json_endpoint(
9747                    &http,
9748                    api_port,
9749                    &format!("node/payloads/size/{avail_block}"),
9750                )
9751                .await?;
9752                assert_json_endpoint(
9753                    &http,
9754                    api_port,
9755                    &format!("node/payloads/size/0/{avail_block}"),
9756                )
9757                .await?;
9758                assert_json_endpoint(
9759                    &http,
9760                    api_port,
9761                    &format!("node/payloads/size/namespace/{avail_ns}"),
9762                )
9763                .await?;
9764                assert_json_endpoint(
9765                    &http,
9766                    api_port,
9767                    &format!("node/payloads/size/namespace/{avail_ns}/{avail_block}"),
9768                )
9769                .await?;
9770                assert_json_endpoint(
9771                    &http,
9772                    api_port,
9773                    &format!("node/payloads/size/namespace/{avail_ns}/0/{avail_block}"),
9774                )
9775                .await?;
9776
9777                assert_json_endpoint(&http, api_port, &format!("node/vid/share/{avail_block}"))
9778                    .await?;
9779                assert_json_endpoint(
9780                    &http,
9781                    api_port,
9782                    &format!("node/vid/share/hash/{block_hash}"),
9783                )
9784                .await?;
9785                assert_json_endpoint(
9786                    &http,
9787                    api_port,
9788                    &format!("node/vid/share/payload-hash/{payload_hash}"),
9789                )
9790                .await?;
9791
9792                assert_json_endpoint(&http, api_port, "node/sync-status").await?;
9793                assert_json_endpoint(&http, api_port, "node/limits").await?;
9794
9795                // Header window: cover all three start variants (time, height, hash). `end` is
9796                // an exclusive Unix-second cutoff; using the block's own timestamp + 1 yields
9797                // a deterministic single-block window.
9798                let avail_ts = avail_header.timestamp();
9799                assert_json_endpoint(
9800                    &http,
9801                    api_port,
9802                    &format!("node/header/window/{avail_ts}/{}", avail_ts + 1),
9803                )
9804                .await?;
9805                assert_json_endpoint(
9806                    &http,
9807                    api_port,
9808                    &format!("node/header/window/from/{avail_block}/{}", avail_ts + 1),
9809                )
9810                .await?;
9811                assert_json_endpoint(
9812                    &http,
9813                    api_port,
9814                    &format!("node/header/window/from/hash/{block_hash}/{}", avail_ts + 1),
9815                )
9816                .await?;
9817
9818                assert_json_endpoint(&http, api_port, "node/stake-table/current").await?;
9819                assert_json_endpoint(&http, api_port, "node/stake-table/1").await?;
9820                assert_json_endpoint(&http, api_port, "node/da-stake-table/current").await?;
9821                assert_json_endpoint(&http, api_port, "node/da-stake-table/1").await?;
9822
9823                assert_json_endpoint(&http, api_port, "node/validators/1").await?;
9824                assert_json_endpoint(&http, api_port, "node/all-validators/1/0/100").await?;
9825
9826                assert_json_endpoint(&http, api_port, "node/participation/proposal/current")
9827                    .await?;
9828                assert_json_endpoint(&http, api_port, "node/participation/proposal/1").await?;
9829                assert_json_endpoint(&http, api_port, "node/participation/vote/current").await?;
9830                assert_json_endpoint(&http, api_port, "node/participation/vote/1").await?;
9831
9832                assert_json_endpoint(&http, api_port, "node/block-reward").await?;
9833                assert_json_endpoint(&http, api_port, "node/block-reward/epoch/1").await?;
9834
9835                assert_json_endpoint(&http, api_port, "node/oldest-block").await?;
9836                assert_json_endpoint(&http, api_port, "node/oldest-leaf").await?;
9837
9838                // Catchup endpoints. View number and height for in-memory state aren't readily
9839                // available after stopping consensus, so we check error semantics on
9840                // intentionally invalid lookups and the deprecated routes.
9841                let decided_view = decided_leaf.view_number().u64();
9842                assert_json_endpoint(
9843                    &http,
9844                    api_port,
9845                    &format!("catchup/{height}/{decided_view}/blocks"),
9846                )
9847                .await?;
9848                // chain-config: a malformed TaggedBase64 commitment (bad checksum) parses-fails
9849                // on the request path and yields 400.
9850                assert_endpoint_status(
9851                    &http,
9852                    api_port,
9853                    "catchup/chain-config/CHAINCONFIG~AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
9854                    400,
9855                )
9856                .await?;
9857                // leafchain: undecided height returns 404 from both.
9858                assert_endpoint_status(&http, api_port, "catchup/999999/leafchain", 404).await?;
9859                // cert2: missing cert returns 404.
9860                assert_endpoint_status(&http, api_port, "catchup/999999/cert2", 404).await?;
9861                // Deprecated catchup routes still respond 404.
9862                assert_endpoint_status(&http, api_port, "catchup/1/reward-amounts/100/0", 404)
9863                    .await?;
9864
9865                // Production peer-catchup posts VBS-binary bodies via http-client.
9866                // Exercise the bulk-account POST endpoints in that exact wire format so any
9867                // regression to "JSON-only body" is caught here.
9868                // Reuse the account sampled above; `validated_state.fee_merkle_tree` was
9869                // captured before the fee-paying blocks and can be empty.
9870                assert_post_binary(
9871                    &http,
9872                    api_port,
9873                    &format!("catchup/{height}/{decided_view}/accounts"),
9874                    &vec![fee_account],
9875                )
9876                .await?;
9877                // reward-accounts V1 takes a Vec<RewardAccountV1>. We send empty since the V2
9878                // tree may not have V1-shaped entries in this test, but the wire format is what
9879                // we're validating.
9880                assert_post_binary(
9881                    &http,
9882                    api_port,
9883                    &format!("catchup/{height}/{decided_view}/reward-accounts"),
9884                    &Vec::<espresso_types::v0_3::RewardAccountV1>::new(),
9885                )
9886                .await?;
9887
9888                // State signature: missing heights should 404.
9889                assert_endpoint_status(&http, api_port, "state-signature/block/999999", 404)
9890                    .await?;
9891
9892                // Error bodies for endpoints that fail via `ApiError` must keep the
9893                // `{"Custom":{"message","status"}}` JSON envelope that existing clients parse.
9894                // Availability endpoints (`availability/leaf/...`, etc.) are excluded because
9895                // they use per-endpoint error variants like `{"FetchLeaf":{...}}`; their status
9896                // codes are still checked by `assert_endpoint_status` above.
9897                assert_error_body(&http, api_port, "catchup/999999/cert2", 404).await?;
9898                assert_error_body(&http, api_port, "catchup/999999/leafchain", 404).await?;
9899                assert_error_body(&http, api_port, "state-signature/block/999999", 404).await?;
9900
9901                // Explorer endpoints.
9902                assert_json_endpoint(&http, api_port, "explorer/explorer-summary").await?;
9903                assert_json_endpoint(&http, api_port, &format!("explorer/block/{avail_block}"))
9904                    .await?;
9905                assert_json_endpoint(
9906                    &http,
9907                    api_port,
9908                    &format!("explorer/block/hash/{block_hash}"),
9909                )
9910                .await?;
9911                assert_json_endpoint(&http, api_port, "explorer/blocks/latest/10").await?;
9912                assert_json_endpoint(
9913                    &http,
9914                    api_port,
9915                    &format!("explorer/blocks/{avail_block}/10"),
9916                )
9917                .await?;
9918                assert_json_endpoint(&http, api_port, "explorer/transactions/latest/10").await?;
9919
9920                // Light-client endpoints. Use the same block we used for availability tests.
9921                assert_json_endpoint(&http, api_port, &format!("light-client/leaf/{avail_block}"))
9922                    .await?;
9923                assert_json_endpoint(
9924                    &http,
9925                    api_port,
9926                    &format!("light-client/leaf/hash/{leaf_hash}"),
9927                )
9928                .await?;
9929                assert_json_endpoint(
9930                    &http,
9931                    api_port,
9932                    &format!("light-client/payload/{avail_block}"),
9933                )
9934                .await?;
9935                assert_json_endpoint(
9936                    &http,
9937                    api_port,
9938                    &format!("light-client/payload/{avail_block}/{}", avail_block + 1),
9939                )
9940                .await?;
9941                assert_json_endpoint(
9942                    &http,
9943                    api_port,
9944                    &format!(
9945                        "light-client/namespace/{avail_block}/{}",
9946                        u64::from(avail_ns)
9947                    ),
9948                )
9949                .await?;
9950                assert_json_endpoint(
9951                    &http,
9952                    api_port,
9953                    &format!(
9954                        "light-client/namespace/{avail_block}/{}/{}",
9955                        avail_block + 1,
9956                        u64::from(avail_ns)
9957                    ),
9958                )
9959                .await?;
9960
9961                // Regression: an oversized range on the plural namespaces route must return
9962                // 400 Bad Request (the status carried by the query-service error), not 500.
9963                let encoded_ns = tagged_base64::TaggedBase64::new(
9964                    ::light_client::client::NAMESPACES_PARAM_TAG,
9965                    &serde_json::to_vec(&vec![u64::from(avail_ns)])?,
9966                )?;
9967                assert_endpoint_status(
9968                    &http,
9969                    api_port,
9970                    &format!(
9971                        "light-client/namespaces/{avail_block}/{}/{encoded_ns}",
9972                        avail_block + 200
9973                    ),
9974                    400,
9975                )
9976                .await?;
9977
9978                // hotshot-events startup info.
9979                assert_json_endpoint(&http, api_port, "hotshot-events/startup_info").await?;
9980
9981                // Token endpoints.
9982                assert_json_endpoint(&http, api_port, "token/total-minted-supply").await?;
9983                assert_json_endpoint(&http, api_port, "token/circulating-supply").await?;
9984                assert_json_endpoint(&http, api_port, "token/circulating-supply-ethereum").await?;
9985                assert_json_endpoint(&http, api_port, "token/total-issued-supply").await?;
9986                assert_json_endpoint(&http, api_port, "token/total-reward-distributed").await?;
9987
9988                // HTTP status codes for common failure cases that clients depend on.
9989
9990                // Requesting a leaf far ahead of the chain tip times out and returns
9991                // 404 Not Found.
9992                assert_endpoint_status(&http, api_port, "availability/leaf/999999", 404).await?;
9993
9994                // Requesting a block range that exceeds the per-request limit
9995                // returns 400 Bad Request.
9996                assert_endpoint_status(
9997                    &http,
9998                    api_port,
9999                    &format!("availability/block/{avail_block}/{}", avail_block + 200),
10000                    400,
10001                )
10002                .await?;
10003
10004                // Requesting a namespace proof range that exceeds the limit also
10005                // returns 400 Bad Request.
10006                assert_endpoint_status(
10007                    &http,
10008                    api_port,
10009                    &format!(
10010                        "availability/block/{avail_block}/{}/namespace/{avail_ns}",
10011                        avail_block + 200
10012                    ),
10013                    400,
10014                )
10015                .await?;
10016            }
10017
10018            anyhow::Ok(())
10019        };
10020
10021        // `block_on` polls the future on the *calling* thread. The default test thread stack is
10022        // 2 MiB on Linux, which isn't enough for this test's large async state machine. We spawn
10023        // a fresh thread with 32 MiB and run the tokio runtime there instead.
10024        std::thread::Builder::new()
10025            .stack_size(32 * 1024 * 1024)
10026            .spawn(move || {
10027                tokio::runtime::Builder::new_multi_thread()
10028                    .enable_all()
10029                    .build()
10030                    .unwrap()
10031                    .block_on(test)
10032                    .unwrap()
10033            })
10034            .unwrap()
10035            .join()
10036            .unwrap()
10037    }
10038
10039    #[test_log::test(tokio::test(flavor = "multi_thread"))]
10040    async fn test_all_validators_endpoint() -> anyhow::Result<()> {
10041        const EPOCH_HEIGHT: u64 = 20;
10042
10043        let network_config = TestConfigBuilder::default()
10044            .epoch_height(EPOCH_HEIGHT)
10045            .build();
10046
10047        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
10048
10049        const NUM_NODES: usize = 5;
10050
10051        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
10052        let persistence: [_; NUM_NODES] = storage
10053            .iter()
10054            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
10055            .collect::<Vec<_>>()
10056            .try_into()
10057            .unwrap();
10058
10059        let config = TestNetworkConfigBuilder::with_num_nodes()
10060            .api_config(SqlDataSource::options(
10061                &storage[0],
10062                Options::with_port(api_port),
10063            ))
10064            .network_config(network_config)
10065            .persistences(persistence.clone())
10066            .catchups(std::array::from_fn(|_| {
10067                StatePeers::<StaticVersion<0, 1>>::from_urls(
10068                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
10069                    Default::default(),
10070                    Duration::from_secs(2),
10071                    &NoMetrics,
10072                )
10073            }))
10074            .pos_hook(
10075                DelegationConfig::MultipleDelegators,
10076                Default::default(),
10077                POS_V4,
10078            )
10079            .await
10080            .unwrap()
10081            .build();
10082
10083        let network = TestNetwork::new(config, POS_V4).await;
10084        let client: Client<ClientErr, SequencerApiVersion> =
10085            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
10086
10087        let err = client
10088            .get::<Vec<RegisteredValidator<PubKey>>>("node/all-validators/1/0/1001")
10089            .header("Accept", "application/json")
10090            .send()
10091            .await
10092            .unwrap_err();
10093
10094        assert_matches!(err, ClientErr { status, message} if
10095                status == StatusCode::BAD_REQUEST
10096                && message.contains("Limit cannot be greater than 1000")
10097        );
10098
10099        // Wait for the chain to progress beyond epoch 3
10100        let mut events = network.peers[0].event_stream();
10101        wait_for_epochs(&mut events, EPOCH_HEIGHT, 3).await;
10102
10103        // Verify that there are no validators for epoch # 1 and epoch # 2
10104        {
10105            client
10106                .get::<Vec<RegisteredValidator<PubKey>>>("node/all-validators/1/0/100")
10107                .send()
10108                .await
10109                .unwrap()
10110                .is_empty();
10111
10112            client
10113                .get::<Vec<RegisteredValidator<PubKey>>>("node/all-validators/2/0/100")
10114                .send()
10115                .await
10116                .unwrap()
10117                .is_empty();
10118        }
10119
10120        // Get the epoch # 3 validators
10121        let validators = client
10122            .get::<Vec<RegisteredValidator<PubKey>>>("node/all-validators/3/0/100")
10123            .send()
10124            .await
10125            .expect("validators");
10126
10127        assert!(!validators.is_empty());
10128
10129        Ok(())
10130    }
10131
10132    #[test_log::test(tokio::test(flavor = "multi_thread"))]
10133    async fn test_reward_accounts_catchup_endpoint() -> anyhow::Result<()> {
10134        const EPOCH_HEIGHT: u64 = 10;
10135        const NUM_NODES: usize = 3;
10136
10137        let network_config = TestConfigBuilder::default()
10138            .epoch_height(EPOCH_HEIGHT)
10139            .build();
10140
10141        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
10142        println!("API PORT = {api_port}");
10143
10144        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
10145        let persistence: [_; NUM_NODES] = storage
10146            .iter()
10147            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
10148            .collect::<Vec<_>>()
10149            .try_into()
10150            .unwrap();
10151
10152        let config = TestNetworkConfigBuilder::with_num_nodes()
10153            .api_config(SqlDataSource::options(
10154                &storage[0],
10155                Options::with_port(api_port).catchup(Default::default()),
10156            ))
10157            .network_config(network_config)
10158            .persistences(persistence.clone())
10159            .catchups(std::array::from_fn(|_| {
10160                StatePeers::<StaticVersion<0, 1>>::from_urls(
10161                    vec![format!("http://localhost:{api_port}").parse().unwrap()],
10162                    Default::default(),
10163                    Duration::from_secs(2),
10164                    &NoMetrics,
10165                )
10166            }))
10167            .pos_hook(
10168                DelegationConfig::MultipleDelegators,
10169                hotshot_contract_adapter::stake_table::StakeTableContractVersion::V3,
10170                POS_V4,
10171            )
10172            .await
10173            .unwrap()
10174            .build();
10175
10176        let mut network = TestNetwork::new(config, POS_V4).await;
10177
10178        let client: Client<ClientErr, StaticVersion<0, 1>> =
10179            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
10180
10181        client.connect(None).await;
10182
10183        let mut events = network.server.event_stream();
10184        wait_for_epochs(&mut events, EPOCH_HEIGHT, 3).await;
10185
10186        network.stop_consensus().await;
10187        let height = network.server.decided_leaf().await.height();
10188        wait_until_block_height(&client, "reward-state-v2/block-height", height).await;
10189
10190        let err = client
10191            .get::<Vec<(RewardAccountV2, RewardAmount)>>(&format!(
10192                "reward-state-v2/reward-amounts/{height}/0/10001"
10193            ))
10194            .send()
10195            .await
10196            .unwrap_err();
10197
10198        assert_matches!(err, ClientErr { status, .. } if
10199            status == StatusCode::BAD_REQUEST
10200
10201        );
10202
10203        let mut expected: Vec<_> = network
10204            .server
10205            .decided_state()
10206            .await
10207            .unwrap()
10208            .reward_merkle_tree_v2
10209            .iter()
10210            .map(|(addr, amt)| (*addr, *amt))
10211            .collect();
10212        // Results are sorted by account address descending
10213        expected.sort_by_key(|(acct, _)| std::cmp::Reverse(*acct));
10214
10215        tracing::info!("expected accounts = {expected:?}");
10216        let limit = expected.len().min(10_000) as u64;
10217        let offset = 0u64;
10218        let expected: Vec<_> = expected.into_iter().take(limit as usize).collect();
10219
10220        let res = client
10221            .get::<Vec<(RewardAccountV2, RewardAmount)>>(&format!(
10222                "reward-state-v2/reward-amounts/{height}/{offset}/{limit}"
10223            ))
10224            .send()
10225            .await
10226            .unwrap();
10227
10228        assert_eq!(res, expected);
10229
10230        Ok(())
10231    }
10232
10233    #[test_log::test(tokio::test(flavor = "multi_thread"))]
10234    async fn test_namespace_query_compat_v0_2() {
10235        test_namespace_query_compat_helper(Upgrade::trivial(FEE_VERSION)).await;
10236    }
10237
10238    #[test_log::test(tokio::test(flavor = "multi_thread"))]
10239    async fn test_namespace_query_compat_v0_3() {
10240        test_namespace_query_compat_helper(Upgrade::trivial(EPOCH_VERSION)).await;
10241    }
10242
10243    async fn test_namespace_query_compat_helper(upgrade: Upgrade) {
10244        // Number of nodes running in the test network.
10245        const NUM_NODES: usize = 5;
10246
10247        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
10248        let url: Url = format!("http://localhost:{port}").parse().unwrap();
10249
10250        let test_config = TestConfigBuilder::default().build();
10251        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
10252            .api_config(Options::from(options::Http {
10253                port,
10254                max_connections: None,
10255                tonic_port: None,
10256            }))
10257            .catchups(std::array::from_fn(|_| {
10258                StatePeers::<SequencerApiVersion>::from_urls(
10259                    vec![url.clone()],
10260                    Default::default(),
10261                    Duration::from_secs(2),
10262                    &NoMetrics,
10263                )
10264            }))
10265            .network_config(test_config)
10266            .build();
10267
10268        let mut network = TestNetwork::new(config, upgrade).await;
10269        let mut events = network.server.event_stream();
10270
10271        // Submit a transaction.
10272        let ns = NamespaceId::from(10_000u64);
10273        let tx = Transaction::new(ns, vec![1, 2, 3]);
10274        network.server.submit_transaction(tx.clone()).await.unwrap();
10275        let block = wait_for_decide_on_handle(&mut events, &tx).await.0;
10276
10277        // Check namespace proof queries.
10278        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
10279        client.connect(None).await;
10280
10281        let (header, common): (Header, VidCommonQueryData<SeqTypes>) = try_join!(
10282            client.get(&format!("availability/header/{block}")).send(),
10283            client
10284                .get(&format!("availability/vid/common/{block}"))
10285                .send()
10286        )
10287        .unwrap();
10288        let version = header.version();
10289
10290        // The latest version of the API (whether we specifically ask for v1 or let the redirect
10291        // occur) will give us a namespace proof no matter which VID version is in use.
10292        for api_ver in ["/v1", ""] {
10293            tracing::info!("test namespace API version: {api_ver}");
10294
10295            let ns_proof: NamespaceProofQueryData = client
10296                .get(&format!(
10297                    "{api_ver}/availability/block/{block}/namespace/{ns}"
10298                ))
10299                .send()
10300                .await
10301                .unwrap();
10302            let proof = ns_proof.proof.as_ref().unwrap();
10303            if version < EPOCH_VERSION {
10304                assert!(matches!(proof, NsProof::V0(..)));
10305            } else {
10306                assert!(matches!(proof, NsProof::V1(..)));
10307            }
10308            let (txs, ns_from_proof) = proof
10309                .verify(
10310                    header.ns_table(),
10311                    &header.payload_commitment(),
10312                    common.common(),
10313                )
10314                .unwrap();
10315            assert_eq!(ns_from_proof, ns);
10316            assert_eq!(txs, ns_proof.transactions);
10317            assert_eq!(txs, std::slice::from_ref(&tx));
10318
10319            // Test range endpoint.
10320            let ns_proofs: Vec<NamespaceProofQueryData> = client
10321                .get(&format!(
10322                    "{api_ver}/availability/block/{}/{}/namespace/{ns}",
10323                    block,
10324                    block + 1
10325                ))
10326                .send()
10327                .await
10328                .unwrap();
10329            assert_eq!(&ns_proofs, std::slice::from_ref(&ns_proof));
10330
10331            // Any API version can correctly tell us that the namespace does not exist.
10332            let ns_proof: NamespaceProofQueryData = client
10333                .get(&format!(
10334                    "{api_ver}/availability/block/{}/namespace/{ns}",
10335                    block - 1
10336                ))
10337                .send()
10338                .await
10339                .unwrap();
10340            assert_eq!(ns_proof.proof, None);
10341            assert_eq!(ns_proof.transactions, vec![]);
10342
10343            // Test streaming.
10344            let mut proofs = client
10345                .socket(&format!(
10346                    "{api_ver}/availability/stream/blocks/0/namespace/{ns}"
10347                ))
10348                .subscribe()
10349                .await
10350                .unwrap();
10351            for i in 0.. {
10352                tracing::info!(i, "stream proof");
10353                let proof: NamespaceProofQueryData = proofs.next().await.unwrap().unwrap();
10354                if proof.proof.is_none() {
10355                    tracing::info!("waiting for non-trivial proof from stream");
10356                    continue;
10357                }
10358                assert_eq!(&proof.transactions, std::slice::from_ref(&tx));
10359                break;
10360            }
10361        }
10362
10363        network.server.shut_down().await;
10364    }
10365
10366    /// Verify the light client leaf, header, and payload proofs at each height
10367    /// against the ground truth captured from the availability streams.
10368    ///
10369    /// Only checks that proofs verify, not which `FinalityProof` variant they
10370    /// use
10371    async fn check_light_client_proofs(
10372        client: &Client<ClientErr, StaticVersion<0, 1>>,
10373        actual_leaves: &[LeafQueryData<SeqTypes>],
10374        actual_blocks: &[BlockQueryData<SeqTypes>],
10375        heights: impl IntoIterator<Item = u64>,
10376        epoch_height: u64,
10377    ) {
10378        let quorum = EpochChangeQuorum::new(epoch_height);
10379        for i in heights {
10380            let leaf = &actual_leaves[i as usize];
10381            let block = &actual_blocks[i as usize];
10382            let version = leaf.header().version();
10383            tracing::info!(i, %version, "check light client proofs");
10384
10385            // Get the same leaf proof by various IDs.
10386            let proofs = try_join_all(
10387                [
10388                    format!("light-client/leaf/{i}"),
10389                    format!("light-client/leaf/hash/{}", leaf.hash()),
10390                    format!("light-client/leaf/block-hash/{}", leaf.block_hash()),
10391                ]
10392                .into_iter()
10393                .map(|path| async move {
10394                    tracing::info!(i, path, "fetch leaf proof");
10395                    let proof = client.get::<LeafProof>(&path).send().await?;
10396                    Ok::<_, anyhow::Error>((path, proof))
10397                }),
10398            )
10399            .await
10400            .unwrap();
10401
10402            // Check proofs against the expected leaf.
10403            for (path, proof) in proofs {
10404                tracing::info!(i, path, "check leaf proof");
10405                assert_eq!(
10406                    proof
10407                        .verify(LeafProofHint::Quorum(&quorum))
10408                        .await
10409                        .unwrap_or_else(|err| panic!("{path}: proof failed to verify: {err:#}")),
10410                    *leaf
10411                );
10412            }
10413
10414            // Get the corresponding header.
10415            let root_height = i + 1;
10416            let root = actual_leaves[root_height as usize].header();
10417            let proofs = try_join_all(
10418                [
10419                    format!("light-client/header/{root_height}/{i}"),
10420                    format!(
10421                        "light-client/header/{root_height}/hash/{}",
10422                        leaf.block_hash()
10423                    ),
10424                ]
10425                .into_iter()
10426                .map(|path| async move {
10427                    tracing::info!(i, path, "fetch header proof");
10428                    let proof = client.get::<HeaderProof>(&path).send().await?;
10429                    Ok::<_, anyhow::Error>((path, proof))
10430                }),
10431            )
10432            .await
10433            .unwrap();
10434            for (path, proof) in proofs {
10435                tracing::info!(i, path, "check header proof");
10436                assert_eq!(
10437                    proof.verify_ref(root.block_merkle_tree_root()).unwrap(),
10438                    leaf.header()
10439                );
10440            }
10441
10442            // Get the corresponding payload.
10443            let proof = client
10444                .get::<PayloadProof>(&format!("light-client/payload/{i}"))
10445                .send()
10446                .await
10447                .unwrap();
10448            assert_eq!(proof.verify(leaf.header()).unwrap(), *block.payload());
10449        }
10450    }
10451
10452    /// Check the light client stake table endpoint: replaying `first_epoch + 2`
10453    /// reproduces the validator set loaded from storage, and an earlier epoch
10454    /// is a `BAD_REQUEST`.
10455    async fn check_light_client_stake_table<N, P>(
10456        client: &Client<ClientErr, StaticVersion<0, 1>>,
10457        server: &SequencerContext<N, P>,
10458        first_epoch: EpochNumber,
10459    ) where
10460        N: ConnectedNetwork<PubKey>,
10461        P: SequencerPersistence,
10462    {
10463        let events: Vec<StakeTableEvent> = client
10464            .get(&format!("light-client/stake-table/{}", first_epoch + 2))
10465            .send()
10466            .await
10467            .unwrap();
10468        let mut state_from_events = StakeTableState::default();
10469        for event in events {
10470            state_from_events.apply_event(event).unwrap().unwrap();
10471        }
10472        assert_eq!(
10473            state_from_events.into_validators(),
10474            server
10475                .consensus_handle()
10476                .storage()
10477                .await
10478                .load_all_validators(first_epoch + 2, 0, 1_000_000)
10479                .await
10480                .unwrap()
10481                .into_iter()
10482                .map(|v| (v.account, v))
10483                .collect::<RegisteredValidatorMap>()
10484        );
10485
10486        // Querying for a stake table before the first real epoch is an error.
10487        let err = client
10488            .get::<Vec<StakeTableEvent>>(&format!("light-client/stake-table/{}", first_epoch + 1))
10489            .send()
10490            .await
10491            .unwrap_err();
10492        assert_eq!(err.status, StatusCode::BAD_REQUEST);
10493    }
10494
10495    #[test_log::test(tokio::test(flavor = "multi_thread"))]
10496    async fn test_light_client_completeness() {
10497        // Run the through a protocol upgrade and epoch change, then check that we are able to get a
10498        // correct light client proof for every finalized leaf.
10499
10500        const NUM_NODES: usize = 1;
10501        const EPOCH_HEIGHT: u64 = 200;
10502
10503        let upgrade = Upgrade::new(LEGACY_VERSION, EPOCH_VERSION);
10504        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
10505        let url: Url = format!("http://localhost:{port}").parse().unwrap();
10506
10507        let test_config = TestConfigBuilder::default()
10508            .epoch_height(EPOCH_HEIGHT)
10509            .epoch_start_block(321)
10510            .set_upgrades(upgrade.target)
10511            .await
10512            .build();
10513
10514        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
10515        let persistence: [_; NUM_NODES] = storage
10516            .iter()
10517            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
10518            .collect::<Vec<_>>()
10519            .try_into()
10520            .unwrap();
10521
10522        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
10523            .api_config(
10524                SqlDataSource::options(&storage[0], Options::with_port(port))
10525                    .light_client(Default::default()),
10526            )
10527            .persistences(persistence.clone())
10528            .catchups(std::array::from_fn(|_| {
10529                StatePeers::<SequencerApiVersion>::from_urls(
10530                    vec![url.clone()],
10531                    Default::default(),
10532                    Duration::from_secs(2),
10533                    &NoMetrics,
10534                )
10535            }))
10536            .network_config(test_config)
10537            .build();
10538
10539        let mut network = TestNetwork::new(config, upgrade).await;
10540        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
10541        client.connect(None).await;
10542
10543        // Get a leaf stream so that we can wait for various events. Also keep track of each leaf
10544        // yielded, which we can use as ground truth later in the test.
10545        let mut actual_leaves = vec![];
10546        let mut actual_blocks = vec![];
10547        let mut leaves = client
10548            .socket("availability/stream/leaves/0")
10549            .subscribe::<LeafQueryData<SeqTypes>>()
10550            .await
10551            .unwrap()
10552            .zip(
10553                client
10554                    .socket("availability/stream/blocks/0")
10555                    .subscribe::<BlockQueryData<SeqTypes>>()
10556                    .await
10557                    .unwrap(),
10558            )
10559            .map(|(leaf, block)| {
10560                let leaf = leaf.unwrap();
10561                let block = block.unwrap();
10562                actual_leaves.push(leaf.clone());
10563                actual_blocks.push(block);
10564                leaf
10565            });
10566
10567        // Wait for the upgrade to take effect.
10568        let (upgrade_height, first_epoch) = loop {
10569            let leaf: LeafQueryData<SeqTypes> = leaves.next().await.unwrap();
10570            if leaf.header().version() < EPOCH_VERSION {
10571                tracing::info!(version = %leaf.header().version(), height = leaf.header().height(), view = ?leaf.leaf().view_number(), "waiting for epoch upgrade");
10572                continue;
10573            }
10574            break (leaf.height(), leaf.leaf().epoch(EPOCH_HEIGHT).unwrap());
10575        };
10576        tracing::info!(upgrade_height, ?first_epoch, "epochs enabled");
10577
10578        // Wait for two epoch changes (so we get to the first epoch that actually uses the stake
10579        // table).
10580        let mut epoch_heights = [0; 2];
10581        for (i, epoch_height) in epoch_heights.iter_mut().enumerate() {
10582            let desired_epoch = first_epoch + (i as u64) + 1;
10583            *epoch_height = loop {
10584                let leaf = leaves.next().await.unwrap();
10585                let epoch = leaf.leaf().epoch(EPOCH_HEIGHT).unwrap();
10586                if epoch > desired_epoch {
10587                    tracing::info!(
10588                        height = leaf.height(),
10589                        ?desired_epoch,
10590                        ?epoch,
10591                        "changed epoch"
10592                    );
10593                    break leaf.height();
10594                }
10595                tracing::info!(
10596                    ?desired_epoch,
10597                    height = leaf.header().height(),
10598                    view = ?leaf.leaf().view_number(),
10599                    "waiting for epoch change"
10600                );
10601            };
10602        }
10603
10604        // Wait a few more blocks.
10605        let max_block = epoch_heights[1] + 1;
10606        loop {
10607            let leaf = leaves.next().await.unwrap();
10608            if leaf.height() > max_block {
10609                break;
10610            }
10611            tracing::info!(max_block, height = leaf.height(), "waiting for block");
10612        }
10613
10614        // Stop consensus. All the blocks we are going to query have already been produced.
10615        // Continuing to run consensus would just waste resources while we check stuff.
10616        network.stop_consensus().await;
10617
10618        // Check light client. Querying every single block is too slow, so we'll check a few blocks
10619        // around various critical points:
10620        let heights =
10621        // * The first few blocks, including genesis
10622            (0..=1)
10623        // * A few blocks just before and after the upgrade
10624            .chain(upgrade_height-1..=upgrade_height+1)
10625        // * A few blocks just before and after the first epoch change
10626            .chain(epoch_heights[0]-1..=epoch_heights[0] + 1)
10627        // * A few blocks just before and after the stake table comes into effect
10628            .chain(epoch_heights[1]-1..=max_block);
10629
10630        check_light_client_proofs(
10631            &client,
10632            &actual_leaves,
10633            &actual_blocks,
10634            heights,
10635            EPOCH_HEIGHT,
10636        )
10637        .await;
10638        check_light_client_stake_table(&client, &network.server, first_epoch).await;
10639    }
10640
10641    /// run through the new protocol upgrade and a following epoch change, then check the
10642    /// light client serves correct leaf, header, payload, and stake table
10643    /// proofs around both boundaries.
10644    #[test_log::test(tokio::test(flavor = "multi_thread"))]
10645    async fn test_light_client_new_protocol_upgrade() {
10646        const NUM_NODES: usize = 5;
10647        const EPOCH_HEIGHT: u64 = 70;
10648        const UPGRADE_START_PROPOSING_VIEW: u64 = 3 * EPOCH_HEIGHT + 5;
10649        const UPGRADE: Upgrade = Upgrade::new(EPOCH_REWARD_VERSION, NEW_PROTOCOL_VERSION);
10650
10651        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
10652        let url: Url = format!("http://localhost:{port}").parse().unwrap();
10653
10654        let test_config = TestConfigBuilder::<NUM_NODES>::default()
10655            .epoch_height(EPOCH_HEIGHT)
10656            .epoch_start_block(0)
10657            .builder_timeout(Duration::from_millis(500))
10658            .set_upgrades(NEW_PROTOCOL_VERSION)
10659            .await
10660            .upgrade_proposing_views(UPGRADE_START_PROPOSING_VIEW, 1000)
10661            .build();
10662
10663        test_config
10664            .anvil()
10665            .expect("TestConfigBuilder starts an anvil")
10666            .anvil_set_interval_mining(1)
10667            .await
10668            .expect("interval mining");
10669
10670        // Base version V5 already has epochs, so genesis must carry the stake
10671        // table contract deployed above.
10672        let genesis_state = ValidatedState {
10673            chain_config: test_config
10674                .get_upgrade_map()
10675                .chain_config(NEW_PROTOCOL_VERSION)
10676                .into(),
10677            ..Default::default()
10678        };
10679
10680        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
10681        let persistence: [_; NUM_NODES] = storage
10682            .iter()
10683            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
10684            .collect::<Vec<_>>()
10685            .try_into()
10686            .unwrap();
10687
10688        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
10689            .api_config(
10690                SqlDataSource::options(&storage[0], Options::with_port(port))
10691                    .light_client(Default::default()),
10692            )
10693            .persistences(persistence)
10694            .states(std::array::from_fn(|_| genesis_state.clone()))
10695            .catchups(std::array::from_fn(|_| {
10696                StatePeers::<SequencerApiVersion>::from_urls(
10697                    vec![url.clone()],
10698                    Default::default(),
10699                    Duration::from_secs(2),
10700                    &NoMetrics,
10701                )
10702            }))
10703            .network_config(test_config)
10704            .build();
10705
10706        let mut network = TestNetwork::new(config, UPGRADE).await;
10707        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
10708        client.connect(None).await;
10709
10710        // Track each leaf and block served by the query service; they are the
10711        // ground truth the light client proofs are checked against.
10712        let mut actual_leaves = vec![];
10713        let mut actual_blocks = vec![];
10714        let mut leaves = client
10715            .socket("availability/stream/leaves/0")
10716            .subscribe::<LeafQueryData<SeqTypes>>()
10717            .await
10718            .unwrap()
10719            .zip(
10720                client
10721                    .socket("availability/stream/blocks/0")
10722                    .subscribe::<BlockQueryData<SeqTypes>>()
10723                    .await
10724                    .unwrap(),
10725            )
10726            .map(|(leaf, block)| {
10727                let leaf = leaf.unwrap();
10728                actual_leaves.push(leaf.clone());
10729                actual_blocks.push(block.unwrap());
10730                leaf
10731            });
10732
10733        // Wait for the upgrade to take effect.
10734        let upgrade_height = timeout(Duration::from_secs(600), async {
10735            loop {
10736                let leaf = leaves.next().await.unwrap();
10737                if leaf.header().version() >= NEW_PROTOCOL_VERSION {
10738                    break leaf.height();
10739                }
10740                tracing::info!(
10741                    version = %leaf.header().version(),
10742                    height = leaf.header().height(),
10743                    view = ?leaf.leaf().view_number(),
10744                    "waiting for new protocol upgrade"
10745                );
10746            }
10747        })
10748        .await
10749        .expect("the network did not upgrade to the new protocol");
10750        let upgrade_epoch = epoch_from_block_number(upgrade_height, EPOCH_HEIGHT);
10751        tracing::info!(upgrade_height, upgrade_epoch, "new protocol enabled");
10752
10753        // Wait for the first post upgrade epoch change, to also cover proofs
10754        // across a V6 epoch boundary
10755        let epoch_change_height = timeout(Duration::from_secs(300), async {
10756            loop {
10757                let leaf = leaves.next().await.unwrap();
10758                let epoch = epoch_from_block_number(leaf.height(), EPOCH_HEIGHT);
10759                if epoch > upgrade_epoch {
10760                    break leaf.height();
10761                }
10762                tracing::info!(
10763                    height = leaf.height(),
10764                    ?epoch,
10765                    "waiting for a post-upgrade epoch change"
10766                );
10767            }
10768        })
10769        .await
10770        .expect("no epoch change happened after the upgrade");
10771        tracing::info!(epoch_change_height, "post upgrade epoch change");
10772
10773        // Run a few more blocks so every queried height has the descendants its
10774        // proof needs (QC chains, header roots, and a finalizing `Certificate2`).
10775        let max_block = epoch_change_height + 3;
10776        timeout(Duration::from_secs(120), async {
10777            loop {
10778                let leaf = leaves.next().await.unwrap();
10779                if leaf.height() > max_block {
10780                    break;
10781                }
10782                tracing::info!(max_block, height = leaf.height(), "waiting for block");
10783            }
10784        })
10785        .await
10786        .expect("the chain stopped making progress after the upgrade");
10787
10788        // Stop consensus: every block we query has already been produced.
10789        network.stop_consensus().await;
10790
10791        // Sample blocks around the two boundaries where proof logic changes
10792        // the V5 -> V6 upgrade and the following V6 epoch change.
10793        let heights =
10794            (upgrade_height - 3..=upgrade_height + 1).chain(epoch_change_height - 1..=max_block);
10795
10796        check_light_client_proofs(
10797            &client,
10798            &actual_leaves,
10799            &actual_blocks,
10800            heights,
10801            EPOCH_HEIGHT,
10802        )
10803        .await;
10804
10805        let client = &client;
10806        let finality_proof = |height: u64| async move {
10807            client
10808                .get::<LeafProof>(&format!("light-client/leaf/{height}"))
10809                .send()
10810                .await
10811                .unwrap()
10812        };
10813        // Everything up to the last two pre cutover leaves is old protocol
10814        for height in upgrade_height - 10..=upgrade_height - 3 {
10815            let proof = finality_proof(height).await;
10816            assert!(
10817                matches!(proof.proof(), FinalityProof::HotStuff2 { .. }),
10818                "leaf {height} should be proven by a HotStuff2 QC chain, got {:?}",
10819                proof.proof(),
10820            );
10821        }
10822
10823        // A post cutover leaf is proven by a new protocol certificate. The last
10824        // two pre cutover leaves will be finalized by new protocol
10825        // e.g cutover at 347 the old protocol decides up to 344 (HotStuff2), and the
10826        // new protocol's first Cert2 directly commits 347 and finalizes
10827        // 345 and 346 with it via the indirect commit rule.
10828        for height in [upgrade_height - 1, epoch_change_height] {
10829            let proof = finality_proof(height).await;
10830            assert!(
10831                matches!(proof.proof(), FinalityProof::NewProtocol { .. }),
10832                "leaf {height} should be proven by a new protocol certificate, got {:?}",
10833                proof.proof(),
10834            );
10835        }
10836
10837        // Epochs run from genesis, so `first_epoch` is 1 and the endpoint is
10838        // queryable from epoch 3, which the chain has long passed.
10839        let first_epoch = EpochNumber::new(epoch_from_block_number(0, EPOCH_HEIGHT));
10840        check_light_client_stake_table(client, &network.server, first_epoch).await;
10841    }
10842
10843    /// Test that `fetch_leaf` returns a leaf with exactly the requested block height.
10844    #[test_log::test(tokio::test(flavor = "multi_thread"))]
10845    async fn test_fetch_leaf_returns_exact_height() -> anyhow::Result<()> {
10846        const EPOCH_HEIGHT: u64 = 10;
10847        const NUM_NODES: usize = 5;
10848        const TARGET_HEIGHT: u64 = EPOCH_HEIGHT * 3 + 2;
10849
10850        let network_config = TestConfigBuilder::default()
10851            .epoch_height(EPOCH_HEIGHT)
10852            .build();
10853
10854        let port = reserve_tcp_port().expect("No ports free for query service");
10855
10856        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
10857        let persistence: [_; NUM_NODES] = storage
10858            .iter()
10859            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
10860            .collect::<Vec<_>>()
10861            .try_into()
10862            .unwrap();
10863
10864        let catchup_peers = std::array::from_fn(|_| {
10865            StatePeers::<StaticVersion<0, 1>>::from_urls(
10866                vec![format!("http://localhost:{port}").parse().unwrap()],
10867                Default::default(),
10868                Duration::from_secs(2),
10869                &NoMetrics,
10870            )
10871        });
10872
10873        let config = TestNetworkConfigBuilder::with_num_nodes()
10874            .api_config(SqlDataSource::options(
10875                &storage[0],
10876                Options::with_port(port),
10877            ))
10878            .network_config(network_config)
10879            .persistences(persistence)
10880            .catchups(catchup_peers)
10881            .pos_hook(
10882                DelegationConfig::MultipleDelegators,
10883                Default::default(),
10884                POS_V4,
10885            )
10886            .await?
10887            .build();
10888
10889        let network = TestNetwork::new(config, POS_V4).await;
10890
10891        // Wait for chain to advance past our target height
10892        let height_client: Client<ClientErr, StaticVersion<0, 1>> =
10893            Client::new(format!("http://localhost:{port}").parse().unwrap());
10894        wait_until_block_height(&height_client, "node/block-height", TARGET_HEIGHT + 5).await;
10895
10896        let coordinator = network.server.node_state().coordinator;
10897
10898        // Use StatePeers to fetch the leaf at the exact target height
10899        let catchup = StatePeers::<StaticVersion<0, 1>>::from_urls(
10900            vec![format!("http://localhost:{port}").parse().unwrap()],
10901            Default::default(),
10902            Duration::from_secs(5),
10903            &NoMetrics,
10904        );
10905
10906        let leaf = catchup.fetch_leaf(coordinator, TARGET_HEIGHT).await?;
10907
10908        assert_eq!(
10909            leaf.height(),
10910            TARGET_HEIGHT,
10911            "fetch_leaf must return the leaf at exactly the requested height"
10912        );
10913
10914        Ok(())
10915    }
10916
10917    /// Start a network at V5 from genesis, restart ALL nodes just before an epoch transition block
10918    /// (`boundary - 3`), and confirm the chain keeps producing blocks afterward.
10919    #[test_log::test(tokio::test(flavor = "multi_thread"))]
10920    async fn test_v5_restart_before_epoch_boundary() {
10921        const NUM_NODES: usize = 3;
10922        const EPOCH_HEIGHT: u64 = 10;
10923        // Rewards start in epoch 4. Restart 3 blocks before the boundary that closes epoch 4, so
10924        // the restarted network produces the boundary block (`4 * EPOCH_HEIGHT`) itself.
10925        const RESTART_EPOCH: u64 = 4;
10926        const RESTART_BOUNDARY: u64 = RESTART_EPOCH * EPOCH_HEIGHT;
10927        const RESTART_HEIGHT: u64 = RESTART_BOUNDARY - 3;
10928        // Blocks to produce after the restart before declaring success.
10929        const BLOCKS_AFTER_RESTART: u64 = 5;
10930
10931        const V5: Upgrade = Upgrade::trivial(EPOCH_REWARD_VERSION);
10932
10933        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
10934
10935        // Slow empty-block production so we comfortably stop at the exact target height. On an idle
10936        // chain the empty-block time is ~`builder_timeout`; raise `next_view_timeout` above it so
10937        // the slow (but healthy) views aren't treated as failures.
10938        let network_config = TestConfigBuilder::<NUM_NODES>::default()
10939            .epoch_height(EPOCH_HEIGHT)
10940            .builder_timeout(Duration::from_secs(3))
10941            .next_view_timeout(Duration::from_secs(10))
10942            .build();
10943
10944        let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await;
10945        let persistence: [_; NUM_NODES] = storage
10946            .iter()
10947            .map(<SqlDataSource as TestableSequencerDataSource>::persistence_options)
10948            .collect::<Vec<_>>()
10949            .try_into()
10950            .unwrap();
10951
10952        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
10953            .api_config(SqlDataSource::options(
10954                &storage[0],
10955                Options::with_port(port),
10956            ))
10957            .persistences(persistence.clone())
10958            .catchups(std::array::from_fn(|_| {
10959                StatePeers::<SequencerApiVersion>::from_urls(
10960                    vec![format!("http://localhost:{port}").parse().unwrap()],
10961                    Default::default(),
10962                    Duration::from_secs(2),
10963                    &NoMetrics,
10964                )
10965            }))
10966            .network_config(network_config)
10967            .pos_hook(DelegationConfig::MultipleDelegators, Default::default(), V5)
10968            .await
10969            .unwrap()
10970            .build();
10971
10972        let mut network = TestNetwork::new(config, V5).await;
10973
10974        // Watch the decide stream and stop as soon as `boundary - 3` is decided. Consuming the
10975        // stream (rather than polling `decided_leaf`) makes this independent of block timing: we
10976        // see every decided leaf and stop the network the moment the target height appears, so we
10977        // can never race past it regardless of how fast blocks are produced.
10978        {
10979            let mut events = network.server.event_stream();
10980            'wait: loop {
10981                let event = events
10982                    .next()
10983                    .await
10984                    .expect("event stream ended unexpectedly");
10985                let CoordinatorEvent::LegacyEvent(Event {
10986                    event: EventType::Decide { leaf_chain, .. },
10987                    ..
10988                }) = event
10989                else {
10990                    continue;
10991                };
10992                // `leaf_chain` is newest-first; once any decided leaf has reached the target
10993                // height, the chain is at or past it.
10994                for LeafInfo { leaf, .. } in leaf_chain.iter() {
10995                    if leaf.block_header().height() >= RESTART_HEIGHT {
10996                        break 'wait;
10997                    }
10998                }
10999            }
11000        }
11001
11002        let restart_height = network.server.decided_leaf().await.height();
11003        tracing::info!(
11004            restart_height,
11005            restart_epoch = RESTART_EPOCH,
11006            restart_boundary = RESTART_BOUNDARY,
11007            "restarting all nodes 3 blocks before an epoch boundary"
11008        );
11009
11010        // Clone the TestConfig before dropping the network so the anvil/L1/contracts stay alive.
11011        let saved_cfg = network.cfg.clone();
11012
11013        network.stop_consensus().await;
11014        drop(network);
11015
11016        // Rebuild reusing the same persistence so nodes resume from stored state.
11017        let port2 = reserve_tcp_port().expect("OS should have ephemeral ports available");
11018        let config2 = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
11019            .api_config(SqlDataSource::options(
11020                &storage[0],
11021                Options::with_port(port2),
11022            ))
11023            .persistences(persistence)
11024            .catchups(std::array::from_fn(|_| {
11025                StatePeers::<SequencerApiVersion>::from_urls(
11026                    vec![format!("http://localhost:{port2}").parse().unwrap()],
11027                    Default::default(),
11028                    Duration::from_secs(2),
11029                    &NoMetrics,
11030                )
11031            }))
11032            .network_config(saved_cfg)
11033            .build();
11034        let network2 = TestNetwork::new(config2, V5).await;
11035
11036        // The restarted network must keep advancing, including across the next epoch boundary.
11037        // Require BLOCKS_AFTER_RESTART new decides, using a lack-of-progress watchdog so a
11038        // healthy-but-slow chain still passes.
11039        let target_height = restart_height + BLOCKS_AFTER_RESTART;
11040        let stall_limit = 30; // 30 polls * 2s = 60s without a new decide => stalled
11041        let mut last_height = network2.server.decided_leaf().await.height();
11042        let mut stalled_polls = 0;
11043        while last_height < target_height {
11044            sleep(Duration::from_secs(2)).await;
11045            let height = network2.server.decided_leaf().await.height();
11046            if height > last_height {
11047                last_height = height;
11048                stalled_polls = 0;
11049            } else {
11050                stalled_polls += 1;
11051                if stalled_polls >= stall_limit {
11052                    panic!(
11053                        "chain stalled after restart 3 blocks before boundary {RESTART_BOUNDARY}: \
11054                         no new decide for 60s at height {last_height}, unable to produce/cross \
11055                         the epoch boundary (target height was {target_height})."
11056                    );
11057                }
11058            }
11059        }
11060    }
11061}