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