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::LightClientDbOptions,
8};
9use alloy::primitives::U256;
10use anyhow::{Context, bail, ensure};
11use async_once_cell::Lazy;
12use async_trait::async_trait;
13use committable::Commitment;
14use data_source::{
15    CatchupDataSource, RequestResponseDataSource, StakeTableDataSource, StateCertDataSource,
16    StateCertFetchingDataSource, SubmitDataSource,
17};
18use derivative::Derivative;
19use espresso_api::{
20    error::SubmitError,
21    v1::{
22        database::{MigrationStatus, TableSize},
23        node::StakeTableWithEpochNumber,
24        status::NodePublicKeys,
25    },
26};
27use espresso_types::{
28    AccountQueryData, AuthenticatedValidatorMap, BlockMerkleTree, ChainId, FeeAccount,
29    FeeMerkleTree, Leaf2, NodeState, PubKey, Transaction,
30    config::PublicNetworkConfig,
31    retain_accounts,
32    traits::{EventsPersistenceRead, MembershipPersistence},
33    v0::traits::{SequencerPersistence, StateCatchup, StateCertPersistence},
34    v0_3::{
35        ChainConfig, RegisteredValidator, RewardAccountQueryDataV1, RewardAccountV1, RewardAmount,
36        RewardMerkleTreeV1, StakeTableEvent,
37    },
38    v0_4::{
39        PermittedRewardMerkleTreeV2, RewardAccountQueryDataV2, RewardAccountV2, RewardMerkleTreeV2,
40    },
41};
42use futures::future::{BoxFuture, Future, FutureExt};
43use hotshot_contract_adapter::sol_types::EspToken;
44use hotshot_query_service::{
45    availability::VidCommonQueryData,
46    data_source::ExtensibleDataSource,
47    fetching::{self, Provider},
48};
49use hotshot_types::{
50    PeerConfig,
51    data::{EpochNumber, VidCommitment, VidCommon, VidShare, ViewNumber},
52    light_client::LCV3StateSignatureRequestBody,
53    network::NetworkConfig,
54    simple_certificate::LightClientStateUpdateCertificateV2,
55    stake_table::HSStakeTable,
56    traits::election::{Membership, MembershipSnapshot},
57    utils::epoch_from_block_number,
58    vid::avidm::{AvidMScheme, init_avidm_param},
59    vote::HasViewNumber,
60};
61use itertools::Itertools;
62use jf_merkle_tree_compat::MerkleTreeScheme;
63use moka::future::Cache;
64use rand::Rng;
65use serde::{Deserialize, Serialize};
66use tokio::{sync::OnceCell, time::timeout};
67use url::Url;
68use vbs::version::Version;
69
70use self::data_source::{
71    HotShotConfigDataSource, NodeKeysDataSource, NodeStateDataSource, StateSignatureDataSource,
72};
73use crate::{
74    SeqTypes,
75    api::{
76        context::{ApiContext, ConsensusSource, NodeLightClient},
77        data_source::TokenDataSource,
78    },
79    catchup::{
80        CatchupStorage, add_fee_accounts_to_state, add_v1_reward_accounts_to_state,
81        add_v2_reward_accounts_to_state,
82    },
83    request_response::{
84        RequestResponseProtocol,
85        data_source::{retain_v1_reward_accounts, retain_v2_reward_accounts},
86        request::{Request, Response},
87    },
88    state_cert::{StateCertFetchError, ensure_cert_can_exist, validate_state_cert},
89};
90#[cfg(any(test, feature = "testing"))]
91use crate::{SequencerApiVersion, SequencerContext};
92
93pub mod context;
94pub mod data_source;
95pub mod fs;
96pub mod light_client;
97pub mod options;
98/// The query service over a SQL database, with one module per backend.
99pub mod sql {
100    pub mod postgres;
101    pub mod sqlite;
102}
103pub mod state;
104pub mod unlock_schedule;
105mod update;
106
107pub use options::Options;
108pub use update::{ApiSink, DecideSink};
109
110pub type BlocksFrontier = <BlockMerkleTree as MerkleTreeScheme>::MembershipProof;
111
112type BoxLazy<T> = Pin<Arc<Lazy<T, BoxFuture<'static, T>>>>;
113
114#[derive(Derivative)]
115#[derivative(Clone(bound = ""), Debug(bound = ""))]
116struct ApiState {
117    // The node context is initialized lazily so we can start the API (and healthcheck endpoints)
118    // before consensus has started. Any endpoint that uses consensus state will wait for
119    // initialization to finish, but endpoints that do not require a consensus handle can proceed
120    // without waiting.
121    #[derivative(Debug = "ignore")]
122    context: BoxLazy<Box<dyn ApiContext>>,
123
124    // we cache `token_supply` for up to an hour, to avoid repeatedly querying the contract for information that rarely changes
125    token_supply: Cache<(), U256>,
126}
127
128impl ApiState {
129    fn new(context_init: impl Future<Output = Box<dyn ApiContext>> + Send + 'static) -> Self {
130        Self {
131            context: Arc::pin(Lazy::from_future(context_init.boxed())),
132            token_supply: Cache::builder()
133                .max_capacity(1)
134                .time_to_live(Duration::from_secs(3600))
135                .build(),
136        }
137    }
138
139    async fn context(&self) -> &dyn ApiContext {
140        self.context.as_ref().get().await.get_ref().as_ref()
141    }
142
143    async fn consensus(&self) -> Arc<dyn ConsensusSource> {
144        self.context().await.consensus()
145    }
146
147    async fn membership(&self) -> Arc<dyn MembershipPersistence> {
148        self.context().await.membership()
149    }
150
151    async fn state_certs(&self) -> Arc<dyn StateCertPersistence> {
152        self.context().await.state_certs()
153    }
154
155    async fn network_config(&self) -> NetworkConfig<SeqTypes> {
156        self.context().await.network_config()
157    }
158}
159
160type StorageState<D> = ExtensibleDataSource<D, ApiState>;
161
162impl<D: Send + Sync> TokenDataSource<SeqTypes> for StorageState<D> {
163    async fn get_initial_supply_l1(&self) -> anyhow::Result<U256> {
164        self.as_ref().get_initial_supply_l1().await
165    }
166
167    async fn get_total_supply_l1(&self) -> anyhow::Result<U256> {
168        self.as_ref().get_total_supply_l1().await
169    }
170
171    async fn get_decided_header(&self) -> espresso_types::Header {
172        self.as_ref().get_decided_header().await
173    }
174}
175
176impl<D: Send + Sync> SubmitDataSource for StorageState<D> {
177    async fn submit(&self, tx: Transaction) -> anyhow::Result<Commitment<Transaction>> {
178        self.as_ref().submit(tx).await
179    }
180}
181
182impl<D: Sync> StakeTableDataSource<SeqTypes> for StorageState<D> {
183    /// Get the stake table for a given epoch
184    async fn get_stake_table(
185        &self,
186        epoch: Option<EpochNumber>,
187    ) -> anyhow::Result<Vec<PeerConfig<SeqTypes>>> {
188        self.as_ref().get_stake_table(epoch).await
189    }
190
191    /// Get the stake table for the current epoch if not provided
192    async fn get_stake_table_current(&self) -> anyhow::Result<StakeTableWithEpochNumber<SeqTypes>> {
193        self.as_ref().get_stake_table_current().await
194    }
195
196    /// Get the DA stake table for a given epoch
197    async fn get_da_stake_table(
198        &self,
199        epoch: Option<EpochNumber>,
200    ) -> anyhow::Result<Vec<PeerConfig<SeqTypes>>> {
201        self.as_ref().get_da_stake_table(epoch).await
202    }
203
204    /// Get the DA stake table for the current epoch if not provided
205    async fn get_da_stake_table_current(
206        &self,
207    ) -> anyhow::Result<StakeTableWithEpochNumber<SeqTypes>> {
208        self.as_ref().get_da_stake_table_current().await
209    }
210
211    /// Get all the validators
212    async fn get_validators(
213        &self,
214        epoch: EpochNumber,
215    ) -> anyhow::Result<AuthenticatedValidatorMap> {
216        self.as_ref().get_validators(epoch).await
217    }
218
219    async fn get_block_reward(
220        &self,
221        epoch: Option<EpochNumber>,
222    ) -> anyhow::Result<Option<RewardAmount>> {
223        self.as_ref().get_block_reward(epoch).await
224    }
225    /// Get all the validator participation for the current epoch
226    async fn current_proposal_participation(&self) -> HashMap<PubKey, f64> {
227        self.as_ref().current_proposal_participation().await
228    }
229    /// Get all the validator participation for the previous epoch
230    async fn proposal_participation(&self, epoch: EpochNumber) -> HashMap<PubKey, f64> {
231        self.as_ref().proposal_participation(epoch).await
232    }
233    /// Get all the vote participation for the current epoch
234    async fn current_vote_participation(&self) -> HashMap<PubKey, f64> {
235        self.as_ref().current_vote_participation().await
236    }
237    /// Get all the vote participation for a given epoch
238    async fn vote_participation(&self, epoch: EpochNumber) -> HashMap<PubKey, f64> {
239        self.as_ref().vote_participation(epoch).await
240    }
241
242    async fn get_all_validators(
243        &self,
244        epoch: EpochNumber,
245        offset: u64,
246        limit: u64,
247    ) -> anyhow::Result<Vec<RegisteredValidator<PubKey>>> {
248        self.as_ref().get_all_validators(epoch, offset, limit).await
249    }
250
251    async fn stake_table_events(
252        &self,
253        from_l1_block: u64,
254        to_l1_block: u64,
255    ) -> anyhow::Result<Vec<StakeTableEvent>> {
256        self.as_ref()
257            .stake_table_events(from_l1_block, to_l1_block)
258            .await
259    }
260}
261
262impl TokenDataSource<SeqTypes> for ApiState {
263    async fn get_initial_supply_l1(&self) -> anyhow::Result<U256> {
264        let node_state = self.context().await.node_state();
265        let fetcher = node_state.coordinator.membership().fetcher().clone();
266        Ok(fetcher.initial_supply_or_fetch().await?)
267    }
268
269    async fn get_total_supply_l1(&self) -> anyhow::Result<U256> {
270        match self.token_supply.get(&()).await {
271            Some(supply) => Ok(supply),
272            None => {
273                let node_state = self.context().await.node_state();
274                let token_contract_address = node_state.token_contract_address().await?;
275
276                let provider = node_state.l1_client.provider;
277
278                let token = EspToken::new(token_contract_address, provider.clone());
279
280                let supply = token
281                    .totalSupply()
282                    .call()
283                    .await
284                    .context("Failed to retrieve totalSupply from the contract")?;
285
286                self.token_supply.insert((), supply).await;
287
288                Ok(supply)
289            },
290        }
291    }
292
293    async fn get_decided_header(&self) -> espresso_types::Header {
294        self.consensus()
295            .await
296            .decided_leaf()
297            .await
298            .block_header()
299            .clone()
300    }
301}
302
303impl StakeTableDataSource<SeqTypes> for ApiState {
304    /// Get the stake table for a given epoch
305    async fn get_stake_table(
306        &self,
307        epoch: Option<EpochNumber>,
308    ) -> anyhow::Result<Vec<PeerConfig<SeqTypes>>> {
309        let handle = self.consensus().await;
310        if let Some(requested) = epoch {
311            let first_epoch = handle
312                .membership_coordinator()
313                .await
314                .membership()
315                .first_epoch();
316            if requested < first_epoch {
317                return Err(anyhow::anyhow!(
318                    "requested stake table for epoch {requested:?} is below the first epoch \
319                     {first_epoch}"
320                ));
321            }
322        }
323        let highest_epoch = handle.current_epoch().await.map(|e| e + 1);
324        if epoch > highest_epoch {
325            return Err(anyhow::anyhow!(
326                "requested stake table for epoch {epoch:?} is beyond the current epoch + 1 \
327                 {highest_epoch:?}"
328            ));
329        }
330        let mem = handle
331            .membership_coordinator()
332            .await
333            .stake_table_for_epoch(epoch)?;
334
335        Ok(mem.stake_table().cloned().collect())
336    }
337
338    /// Get the stake table for the current epoch and return it along with the epoch number
339    async fn get_stake_table_current(&self) -> anyhow::Result<StakeTableWithEpochNumber<SeqTypes>> {
340        let epoch = self.consensus().await.current_epoch().await;
341
342        Ok(StakeTableWithEpochNumber {
343            epoch,
344            stake_table: self.get_stake_table(epoch).await?,
345        })
346    }
347
348    /// Get the DA stake table for a given epoch
349    async fn get_da_stake_table(
350        &self,
351        epoch: Option<EpochNumber>,
352    ) -> anyhow::Result<Vec<PeerConfig<SeqTypes>>> {
353        let coordinator = self.consensus().await.membership_coordinator().await;
354        Ok(match epoch {
355            Some(e) => coordinator
356                .membership()
357                .snapshot(e)
358                .map(|s| s.da_stake_table().cloned().collect())
359                .unwrap_or_default(),
360            None => coordinator
361                .membership()
362                .genesis_stake_tables()
363                .da_stake_table()
364                .cloned()
365                .collect(),
366        })
367    }
368
369    /// Get the DA stake table for the current epoch and return it along with the epoch number
370    async fn get_da_stake_table_current(
371        &self,
372    ) -> anyhow::Result<StakeTableWithEpochNumber<SeqTypes>> {
373        let epoch = self.consensus().await.current_epoch().await;
374
375        Ok(StakeTableWithEpochNumber {
376            epoch,
377            stake_table: self.get_da_stake_table(epoch).await?,
378        })
379    }
380
381    async fn get_block_reward(
382        &self,
383        epoch: Option<EpochNumber>,
384    ) -> anyhow::Result<Option<RewardAmount>> {
385        let coordinator = self.consensus().await.membership_coordinator().await;
386
387        let membership = coordinator.membership();
388        let block_reward = match epoch {
389            None => membership.fixed_block_reward(),
390            Some(e) => membership.epoch_block_reward(e),
391        };
392
393        Ok(block_reward)
394    }
395
396    /// Get the whole validators map
397    async fn get_validators(&self, e: EpochNumber) -> anyhow::Result<AuthenticatedValidatorMap> {
398        Ok(self
399            .consensus()
400            .await
401            .membership_coordinator()
402            .await
403            .membership_for_epoch(Some(e))
404            .context("membership not found")?
405            .snapshot()
406            .with_context(|| format!("no committee for epoch={e}"))?
407            .validators()
408            .clone())
409    }
410
411    /// Get the current proposal participation.
412    async fn current_proposal_participation(&self) -> HashMap<PubKey, f64> {
413        self.consensus()
414            .await
415            .current_proposal_participation()
416            .await
417    }
418
419    /// Get the proposal participation for a given epoch.
420    async fn proposal_participation(&self, epoch: EpochNumber) -> HashMap<PubKey, f64> {
421        self.consensus().await.proposal_participation(epoch).await
422    }
423
424    /// Get the current vote participation.
425    async fn current_vote_participation(&self) -> HashMap<PubKey, f64> {
426        self.consensus().await.current_vote_participation().await
427    }
428
429    /// Get the vote participation for a given epoch.
430    async fn vote_participation(&self, epoch: EpochNumber) -> HashMap<PubKey, f64> {
431        self.consensus().await.vote_participation(epoch).await
432    }
433
434    async fn get_all_validators(
435        &self,
436        epoch: EpochNumber,
437        offset: u64,
438        limit: u64,
439    ) -> anyhow::Result<Vec<RegisteredValidator<PubKey>>> {
440        self.membership()
441            .await
442            .load_all_validators(epoch, offset, limit)
443            .await
444    }
445
446    async fn stake_table_events(
447        &self,
448        from_l1_block: u64,
449        to_l1_block: u64,
450    ) -> anyhow::Result<Vec<StakeTableEvent>> {
451        let (status, events) = self
452            .membership()
453            .await
454            .load_events(from_l1_block, to_l1_block)
455            .await?;
456        ensure!(
457            status == Some(EventsPersistenceRead::Complete),
458            "some events in range [{from_l1_block}, {to_l1_block}] are not available ({status:?})"
459        );
460        Ok(events.into_iter().map(|(_, event)| event).collect())
461    }
462}
463
464impl<D: Sync> RequestResponseDataSource<SeqTypes> for StorageState<D> {
465    async fn request_vid_shares(
466        &self,
467        block_number: u64,
468        vid_common_data: VidCommonQueryData<SeqTypes>,
469        timeout_duration: Duration,
470    ) -> BoxFuture<'static, anyhow::Result<Vec<VidShare>>> {
471        self.as_ref()
472            .request_vid_shares(block_number, vid_common_data, timeout_duration)
473            .await
474    }
475}
476
477#[async_trait]
478impl<D: Sync> StateCertFetchingDataSource<SeqTypes> for StorageState<D> {
479    async fn request_state_cert(
480        &self,
481        epoch: u64,
482        timeout: Duration,
483    ) -> Result<LightClientStateUpdateCertificateV2<SeqTypes>, StateCertFetchError> {
484        self.as_ref().request_state_cert(epoch, timeout).await
485    }
486}
487
488impl RequestResponseDataSource<SeqTypes> for ApiState {
489    async fn request_vid_shares(
490        &self,
491        block_number: u64,
492        vid_common_data: VidCommonQueryData<SeqTypes>,
493        duration: Duration,
494    ) -> BoxFuture<'static, anyhow::Result<Vec<VidShare>>> {
495        self.context()
496            .await
497            .request_vid_shares(block_number, vid_common_data, duration)
498    }
499}
500
501pub(super) fn request_vid_shares<P>(
502    request_response_protocol: RequestResponseProtocol<P>,
503    block_number: u64,
504    vid_common_data: VidCommonQueryData<SeqTypes>,
505    duration: Duration,
506) -> BoxFuture<'static, anyhow::Result<Vec<VidShare>>>
507where
508    P: SequencerPersistence,
509{
510    async move {
511        let total_weight = match vid_common_data.common() {
512            VidCommon::V0(_) => {
513                // TODO: This needs to be done via the stake table
514                return Err(anyhow::anyhow!(
515                    "V0 total weight calculation not supported yet"
516                ));
517            },
518            VidCommon::V1(v1) => v1.total_weights,
519            VidCommon::V2(v2) => v2.param.total_weights,
520        };
521
522        let avidm_param =
523            init_avidm_param(total_weight).with_context(|| "failed to initialize avidm param")?;
524
525        let VidCommitment::V1(local_payload_hash) = vid_common_data.payload_hash() else {
526            bail!("V0 share verification not supported yet");
527        };
528
529        let request_id = rand::thread_rng().r#gen();
530        let received_shares = Arc::new(parking_lot::Mutex::new(Vec::new()));
531        let received_shares_clone = received_shares.clone();
532        let request_result: anyhow::Result<_, _> = timeout(
533            duration,
534            request_response_protocol.request_indefinitely::<_, _, _>(
535                Request::VidShare(block_number, request_id),
536                move |_request, response| {
537                    let avidm_param = avidm_param.clone();
538                    let received_shares = received_shares_clone.clone();
539                    async move {
540                        let Response::VidShare(VidShare::V1(received_share)) = response else {
541                            bail!("V0 share verification not supported yet");
542                        };
543                        let Ok(Ok(_)) = AvidMScheme::verify_share(
544                            &avidm_param,
545                            &local_payload_hash,
546                            &received_share,
547                        ) else {
548                            bail!("share verification failed");
549                        };
550                        received_shares.lock().push(received_share);
551
552                        bail!("waiting for more shares");
553
554                        #[allow(unreachable_code)]
555                        Ok(())
556                    }
557                },
558            ),
559        )
560        .await;
561
562        match request_result {
563            // Timing out is the success path: the request runs until the deadline.
564            Err(_) => Ok(received_shares
565                .lock()
566                .clone()
567                .into_iter()
568                .map(VidShare::V1)
569                .collect()),
570            Ok(Err(e)) => Err(e).with_context(|| "failed to request vid shares"),
571            Ok(Ok(_)) => bail!("this should not be possible"),
572        }
573    }
574    .boxed()
575}
576
577#[async_trait]
578impl StateCertFetchingDataSource<SeqTypes> for ApiState {
579    async fn request_state_cert(
580        &self,
581        epoch: u64,
582        timeout: Duration,
583    ) -> Result<LightClientStateUpdateCertificateV2<SeqTypes>, StateCertFetchError> {
584        let handle = self.consensus().await;
585        let coordinator = handle.membership_coordinator().await;
586        let epoch_height = *coordinator.epoch_height();
587        let decided = handle.decided_leaf().await;
588        ensure_cert_can_exist(
589            EpochNumber::new(epoch),
590            coordinator.membership().first_epoch(),
591            epoch_height,
592            decided.epoch(epoch_height).map(|_| decided.height()),
593            handle.current_epoch().await,
594        )?;
595
596        tracing::info!("fetching state certificate for epoch={epoch}");
597        if let Err(err) = coordinator.stake_table_for_epoch(Some(EpochNumber::new(epoch))) {
598            tracing::warn!(
599                "Failed to get membership for epoch {epoch}: {err:#}. Waiting for catchup"
600            );
601
602            coordinator
603                .wait_for_catchup(EpochNumber::new(epoch))
604                .await
605                .map_err(|e| {
606                    StateCertFetchError::Other(
607                        anyhow::Error::new(e)
608                            .context(format!("failed to catch up for stake table epoch={epoch}")),
609                    )
610                })?;
611        }
612
613        let membership = coordinator
614            .stake_table_for_epoch(Some(EpochNumber::new(epoch)))
615            .map_err(|e| {
616                StateCertFetchError::Other(
617                    anyhow::Error::new(e)
618                        .context(format!("failed to get stake table for epoch={epoch}")),
619                )
620            })?;
621
622        let stake_table = HSStakeTable::from_iter(membership.stake_table());
623
624        let state_catchup = self.context().await.node_state().state_catchup.clone();
625
626        let result = tokio::time::timeout(timeout, state_catchup.fetch_state_cert(epoch)).await;
627
628        match result {
629            Err(_) => Err(StateCertFetchError::FetchError(anyhow::anyhow!(
630                "timeout while fetching state cert for epoch {epoch}"
631            ))),
632            Ok(Ok(cert)) => {
633                // Validation errors should be mapped to ValidationError
634                validate_state_cert(
635                    &cert,
636                    &stake_table,
637                    EpochNumber::new(epoch),
638                    epoch_height,
639                    &handle.upgrade_lock().await,
640                )
641                .map_err(|e| {
642                    StateCertFetchError::ValidationError(e.context(format!("epoch={epoch}")))
643                })?;
644
645                tracing::info!("fetched and validated state certificate for epoch {epoch}");
646                Ok(cert)
647            },
648            Ok(Err(e)) => Err(StateCertFetchError::FetchError(
649                e.context(format!("epoch={epoch}")),
650            )),
651        }
652    }
653}
654
655// Thin wrapper implementations that delegate to persistence
656#[async_trait]
657impl<D: Sync> StateCertDataSource for StorageState<D> {
658    async fn get_state_cert_by_epoch(
659        &self,
660        epoch: u64,
661    ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
662        self.as_ref().get_state_cert_by_epoch(epoch).await
663    }
664
665    async fn insert_state_cert(
666        &self,
667        epoch: u64,
668        cert: LightClientStateUpdateCertificateV2<SeqTypes>,
669    ) -> anyhow::Result<()> {
670        self.as_ref().insert_state_cert(epoch, cert).await
671    }
672}
673
674#[async_trait]
675impl StateCertDataSource for ApiState {
676    async fn get_state_cert_by_epoch(
677        &self,
678        epoch: u64,
679    ) -> anyhow::Result<Option<LightClientStateUpdateCertificateV2<SeqTypes>>> {
680        self.state_certs()
681            .await
682            .get_state_cert_by_epoch(epoch)
683            .await
684    }
685
686    async fn insert_state_cert(
687        &self,
688        epoch: u64,
689        cert: LightClientStateUpdateCertificateV2<SeqTypes>,
690    ) -> anyhow::Result<()> {
691        self.state_certs()
692            .await
693            .insert_state_cert(epoch, cert)
694            .await
695    }
696}
697
698impl SubmitDataSource for ApiState {
699    async fn submit(&self, tx: Transaction) -> anyhow::Result<Commitment<Transaction>> {
700        let handle = self.consensus().await;
701
702        // Fetch full chain config from the validated state, if present.
703        // This is necessary because we support chain config upgrades,
704        // so the updated chain config is found in the validated state.
705        let cf = handle
706            .decided_state()
707            .await
708            .and_then(|state| state.chain_config.resolve());
709
710        // Use the chain config from the validated state if available,
711        // otherwise, use the node state's chain config
712        // The node state's chain config is the node's base version chain config
713        let cf = match cf {
714            Some(cf) => cf,
715            None => self.node_state().await.chain_config,
716        };
717
718        let max_block_size: u64 = cf.max_block_size.into();
719        let txn_size = tx.payload().len() as u64;
720
721        // reject transaction bigger than block size
722        if txn_size > max_block_size {
723            return Err(SubmitError::Invalid(format!(
724                "transaction size ({txn_size}) is greater than max_block_size ({max_block_size})"
725            ))
726            .into());
727        }
728
729        handle.submit_transaction(tx).await
730    }
731}
732
733impl<D: Sync> NodeStateDataSource for StorageState<D> {
734    async fn node_state(&self) -> NodeState {
735        self.as_ref().node_state().await
736    }
737}
738
739impl<D: CatchupStorage + Send + Sync> data_source::DatabaseMetadataSource for StorageState<D>
740where
741    D: data_source::DatabaseMetadataSource + Send + Sync,
742{
743    async fn get_table_sizes(&self) -> anyhow::Result<Vec<TableSize>> {
744        self.inner().get_table_sizes().await
745    }
746
747    async fn get_migration_status(&self) -> anyhow::Result<Vec<MigrationStatus>> {
748        self.inner().get_migration_status().await
749    }
750}
751
752impl<D: CatchupStorage + Send + Sync> data_source::PruningDataSource for StorageState<D>
753where
754    D: data_source::PruningDataSource + Send + Sync,
755{
756    async fn get_oldest_block(
757        &self,
758    ) -> anyhow::Result<Option<hotshot_query_service::availability::BlockQueryData<crate::SeqTypes>>>
759    {
760        self.inner().get_oldest_block().await
761    }
762
763    async fn get_oldest_leaf(
764        &self,
765    ) -> anyhow::Result<Option<hotshot_query_service::availability::LeafQueryData<crate::SeqTypes>>>
766    {
767        self.inner().get_oldest_leaf().await
768    }
769}
770
771impl<D: CatchupStorage + Send + Sync> CatchupDataSource for StorageState<D> {
772    #[tracing::instrument(skip(self, instance))]
773    async fn get_accounts(
774        &self,
775        instance: &NodeState,
776        height: u64,
777        view: ViewNumber,
778        accounts: &[FeeAccount],
779    ) -> anyhow::Result<FeeMerkleTree> {
780        // Check if we have the desired state in memory.
781        match self
782            .as_ref()
783            .get_accounts(instance, height, view, accounts)
784            .await
785        {
786            Ok(accounts) => return Ok(accounts),
787            Err(err) => {
788                tracing::info!("accounts not in memory, trying storage: {err:#}");
789            },
790        }
791
792        // Try storage.
793        let (tree, leaf) = self
794            .inner()
795            .get_accounts(instance, height, view, accounts)
796            .await
797            .context("accounts not in memory, and could not fetch from storage")?;
798        // If we successfully fetched accounts from storage, try to add them back into the in-memory
799        // state.
800
801        let handle = self.as_ref().consensus().await;
802        if let Err(err) = add_fee_accounts_to_state(&*handle, &view, accounts, &tree, leaf).await {
803            tracing::warn!(?view, "cannot update fetched account state: {err:#}");
804        }
805        tracing::info!(?view, "updated with fetched account state");
806
807        Ok(tree)
808    }
809
810    #[tracing::instrument(skip(self, instance))]
811    async fn get_frontier(
812        &self,
813        instance: &NodeState,
814        height: u64,
815        view: ViewNumber,
816    ) -> anyhow::Result<BlocksFrontier> {
817        // Check if we have the desired state in memory.
818        match self.as_ref().get_frontier(instance, height, view).await {
819            Ok(frontier) => return Ok(frontier),
820            Err(err) => {
821                tracing::info!("frontier is not in memory, trying storage: {err:#}");
822            },
823        }
824
825        // Try storage.
826        self.inner().get_frontier(instance, height, view).await
827    }
828
829    async fn get_chain_config(
830        &self,
831        commitment: Commitment<ChainConfig>,
832    ) -> anyhow::Result<ChainConfig> {
833        // Check if we have the desired state in memory.
834        match self.as_ref().get_chain_config(commitment).await {
835            Ok(cf) => return Ok(cf),
836            Err(err) => {
837                tracing::info!("chain config is not in memory, trying storage: {err:#}");
838            },
839        }
840
841        // Try storage.
842        self.inner().get_chain_config(commitment).await
843    }
844    async fn get_leaf_chain(&self, height: u64) -> anyhow::Result<Vec<Leaf2>> {
845        // Check if we have the desired state in memory.
846        match self.as_ref().get_leaf_chain(height).await {
847            Ok(cf) => return Ok(cf),
848            Err(err) => {
849                tracing::info!("leaf chain is not in memory, trying storage: {err:#}");
850            },
851        }
852
853        // Try storage.
854        self.inner().get_leaf_chain(height).await
855    }
856
857    async fn get_cert2(
858        &self,
859        height: u64,
860    ) -> anyhow::Result<Option<espresso_types::Certificate2<SeqTypes>>> {
861        self.inner().load_cert2(height).await
862    }
863
864    #[tracing::instrument(skip(self, instance))]
865    async fn get_reward_accounts_v2(
866        &self,
867        instance: &NodeState,
868        height: u64,
869        view: ViewNumber,
870        accounts: &[RewardAccountV2],
871    ) -> anyhow::Result<RewardMerkleTreeV2> {
872        // Check if we have the desired state in memory.
873        match self
874            .as_ref()
875            .get_reward_accounts_v2(instance, height, view, accounts)
876            .await
877        {
878            Ok(accounts) => return Ok(accounts),
879            Err(err) => {
880                tracing::info!("reward accounts not in memory, trying storage: {err:#}");
881            },
882        }
883
884        // Try storage.
885        let (tree, leaf) = self
886            .inner()
887            .get_reward_accounts_v2(instance, height, view, accounts)
888            .await
889            .context("accounts not in memory, and could not fetch from storage")?;
890
891        // If we successfully fetched accounts from storage, try to add them back into the in-memory
892        // state.
893        let handle = self.as_ref().consensus().await;
894        if let Err(err) =
895            add_v2_reward_accounts_to_state(&*handle, &view, accounts, &tree, leaf).await
896        {
897            tracing::warn!(?view, "cannot update fetched account state: {err:#}");
898        }
899        tracing::info!(?view, "updated with fetched account state");
900
901        Ok(tree)
902    }
903
904    #[tracing::instrument(skip(self, instance))]
905    async fn get_reward_accounts_v1(
906        &self,
907        instance: &NodeState,
908        height: u64,
909        view: ViewNumber,
910        accounts: &[RewardAccountV1],
911    ) -> anyhow::Result<RewardMerkleTreeV1> {
912        // Check if we have the desired state in memory.
913        match self
914            .as_ref()
915            .get_reward_accounts_v1(instance, height, view, accounts)
916            .await
917        {
918            Ok(accounts) => return Ok(accounts),
919            Err(err) => {
920                tracing::info!("reward accounts not in memory, trying storage: {err:#}");
921            },
922        }
923
924        // Try storage.
925        let (tree, leaf) = self
926            .inner()
927            .get_reward_accounts_v1(instance, height, view, accounts)
928            .await
929            .context("accounts not in memory, and could not fetch from storage")?;
930
931        // If we successfully fetched accounts from storage, try to add them back into the in-memory
932        // state.
933        let handle = self.as_ref().consensus().await;
934        if let Err(err) =
935            add_v1_reward_accounts_to_state(&*handle, &view, accounts, &tree, leaf).await
936        {
937            tracing::warn!(?view, "cannot update fetched account state: {err:#}");
938        }
939        tracing::info!(?view, "updated with fetched account state");
940
941        Ok(tree)
942    }
943
944    async fn get_reward_merkle_tree_v2(
945        &self,
946        height: u64,
947        view: ViewNumber,
948    ) -> anyhow::Result<Vec<u8>> {
949        match self.as_ref().get_reward_merkle_tree_v2(height, view).await {
950            Ok(tree) => return Ok(tree),
951            Err(err) => {
952                tracing::info!("reward merkle tree is not in memory, trying storage: {err:#}");
953            },
954        }
955        self.inner()
956            .load_serialized_reward_merkle_tree_v2(height)
957            .await
958    }
959
960    #[tracing::instrument(skip(self))]
961    async fn get_state_cert(
962        &self,
963        epoch: u64,
964    ) -> anyhow::Result<LightClientStateUpdateCertificateV2<SeqTypes>> {
965        self.as_ref()
966            .state_certs()
967            .await
968            .get_state_cert_by_epoch(epoch)
969            .await?
970            .context(format!("state cert for epoch {epoch} not found"))
971    }
972}
973
974impl NodeStateDataSource for ApiState {
975    async fn node_state(&self) -> NodeState {
976        self.context().await.node_state()
977    }
978}
979
980impl CatchupDataSource for ApiState {
981    #[tracing::instrument(skip(self, _instance))]
982    async fn get_accounts(
983        &self,
984        _instance: &NodeState,
985        height: u64,
986        view: ViewNumber,
987        accounts: &[FeeAccount],
988    ) -> anyhow::Result<FeeMerkleTree> {
989        let state = self.consensus().await.state(view).await.context(format!(
990            "state not available for height {height}, view {view}"
991        ))?;
992        retain_accounts(&state.fee_merkle_tree, accounts.iter().copied())
993    }
994
995    #[tracing::instrument(skip(self, _instance))]
996    async fn get_frontier(
997        &self,
998        _instance: &NodeState,
999        height: u64,
1000        view: ViewNumber,
1001    ) -> anyhow::Result<BlocksFrontier> {
1002        let state = self.consensus().await.state(view).await.context(format!(
1003            "state not available for height {height}, view {view}"
1004        ))?;
1005        let tree = &state.block_merkle_tree;
1006        let frontier = tree.lookup(tree.num_leaves() - 1).expect_ok()?.1;
1007        Ok(frontier)
1008    }
1009
1010    async fn get_chain_config(
1011        &self,
1012        commitment: Commitment<ChainConfig>,
1013    ) -> anyhow::Result<ChainConfig> {
1014        let state = self
1015            .consensus()
1016            .await
1017            .decided_state()
1018            .await
1019            .context("decided state not available")?;
1020        let chain_config = state.chain_config;
1021
1022        if chain_config.commit() == commitment {
1023            chain_config.resolve().context("chain config found")
1024        } else {
1025            bail!("chain config not found")
1026        }
1027    }
1028
1029    async fn get_leaf_chain(&self, height: u64) -> anyhow::Result<Vec<Leaf2>> {
1030        // Builds a legacy 3-chain from undecided leaves in memory. New-protocol heights fall
1031        // through to the storage path.
1032        let mut leaves = self.consensus().await.undecided_leaves().await;
1033        leaves.sort_by_key(|l| l.view_number());
1034        let (position, mut last_leaf) = leaves
1035            .iter()
1036            .find_position(|l| l.height() == height)
1037            .context(format!("leaf chain not available for {height}"))?;
1038        let mut chain = vec![last_leaf.clone()];
1039        for leaf in leaves.iter().skip(position + 1) {
1040            if leaf.justify_qc().view_number() == last_leaf.view_number() {
1041                chain.push(leaf.clone());
1042            } else {
1043                continue;
1044            }
1045            if leaf.view_number() == last_leaf.view_number() + 1 {
1046                // one away from decide
1047                last_leaf = leaf;
1048                break;
1049            }
1050            last_leaf = leaf;
1051        }
1052        // Make sure we got one more leaf to confirm the decide
1053        for leaf in leaves
1054            .iter()
1055            .skip_while(|l| l.view_number() <= last_leaf.view_number())
1056        {
1057            if leaf.justify_qc().view_number() == last_leaf.view_number() {
1058                chain.push(leaf.clone());
1059                return Ok(chain);
1060            }
1061        }
1062        bail!(format!("leaf chain not available for {height}"))
1063    }
1064
1065    #[tracing::instrument(skip(self, _instance))]
1066    async fn get_reward_accounts_v2(
1067        &self,
1068        _instance: &NodeState,
1069        height: u64,
1070        view: ViewNumber,
1071        accounts: &[RewardAccountV2],
1072    ) -> anyhow::Result<RewardMerkleTreeV2> {
1073        let state = self.consensus().await.state(view).await.context(format!(
1074            "state not available for height {height}, view {view}"
1075        ))?;
1076
1077        retain_v2_reward_accounts(&state.reward_merkle_tree_v2, accounts.iter().copied())
1078    }
1079
1080    #[tracing::instrument(skip(self, _instance))]
1081    async fn get_reward_accounts_v1(
1082        &self,
1083        _instance: &NodeState,
1084        height: u64,
1085        view: ViewNumber,
1086        accounts: &[RewardAccountV1],
1087    ) -> anyhow::Result<RewardMerkleTreeV1> {
1088        let state = self.consensus().await.state(view).await.context(format!(
1089            "state not available for height {height}, view {view}"
1090        ))?;
1091
1092        retain_v1_reward_accounts(&state.reward_merkle_tree_v1, accounts.iter().copied())
1093    }
1094
1095    async fn get_reward_merkle_tree_v2(
1096        &self,
1097        height: u64,
1098        view: ViewNumber,
1099    ) -> anyhow::Result<Vec<u8>> {
1100        let state = self.consensus().await.state(view).await.context(format!(
1101            "state not available for height {height}, view {view}"
1102        ))?;
1103
1104        let tree_data = TryInto::<RewardMerkleTreeV2Data>::try_into(&state.reward_merkle_tree_v2)
1105            .inspect_err(
1106            |err| tracing::debug!(%err, height, %view, "cannot serve reward merkle tree"),
1107        )?;
1108        let merkle_tree_bytes = bincode::serialize(&tree_data)
1109            .context("Merkle tree serialization failed; this should never happen.")?;
1110
1111        Ok(merkle_tree_bytes)
1112    }
1113
1114    async fn get_state_cert(
1115        &self,
1116        epoch: u64,
1117    ) -> anyhow::Result<LightClientStateUpdateCertificateV2<SeqTypes>> {
1118        self.get_state_cert_by_epoch(epoch)
1119            .await?
1120            .context(format!("state cert not found for epoch {epoch}"))
1121    }
1122}
1123
1124impl<D: Sync> HotShotConfigDataSource for StorageState<D> {
1125    async fn get_config(&self) -> PublicNetworkConfig {
1126        self.as_ref().network_config().await.into()
1127    }
1128}
1129
1130impl HotShotConfigDataSource for ApiState {
1131    async fn get_config(&self) -> PublicNetworkConfig {
1132        self.network_config().await.into()
1133    }
1134}
1135
1136impl<D: Sync> NodeKeysDataSource for StorageState<D> {
1137    async fn node_public_keys(&self) -> Option<NodePublicKeys> {
1138        self.as_ref().node_public_keys().await
1139    }
1140}
1141
1142impl NodeKeysDataSource for ApiState {
1143    async fn node_public_keys(&self) -> Option<NodePublicKeys> {
1144        let ctx = self.context().await;
1145        let config = ctx.validator_config()?;
1146        let consensus_key = config.public_key;
1147        let eth_account = ctx
1148            .consensus()
1149            .membership_coordinator()
1150            .await
1151            .membership()
1152            .latest_account(&consensus_key);
1153        Some(NodePublicKeys {
1154            eth_account,
1155            consensus_key,
1156            state_ver_key: config.state_public_key.clone(),
1157            x25519_key: config.x25519_keypair.as_ref().map(|kp| kp.public_key()),
1158            p2p_addr: config.p2p_addr.clone(),
1159        })
1160    }
1161}
1162
1163#[async_trait]
1164impl<D: Sync> StateSignatureDataSource for StorageState<D> {
1165    async fn get_state_signature(&self, height: u64) -> Option<LCV3StateSignatureRequestBody> {
1166        self.as_ref().get_state_signature(height).await
1167    }
1168}
1169
1170#[async_trait]
1171impl StateSignatureDataSource for ApiState {
1172    async fn get_state_signature(&self, height: u64) -> Option<LCV3StateSignatureRequestBody> {
1173        self.context()
1174            .await
1175            .state_signatures()?
1176            .read()
1177            .await
1178            .get_signature(height)
1179    }
1180}
1181
1182#[derive(Serialize, Deserialize, Debug, Clone)]
1183/// Representation of the RewardMerkleTreeV2 as a set of key-value pairs
1184pub struct RewardMerkleTreeV2Data {
1185    pub balances: Vec<(RewardAccountV2, RewardAmount)>,
1186}
1187
1188impl TryInto<RewardMerkleTreeV2Data> for &RewardMerkleTreeV2 {
1189    type Error = anyhow::Error;
1190    // Required method
1191    fn try_into(self) -> anyhow::Result<RewardMerkleTreeV2Data> {
1192        let num_leaves = self.num_leaves();
1193
1194        let balances: Vec<_> = self
1195            .iter()
1196            .map(|(account, balance)| (*account, *balance))
1197            .collect();
1198
1199        if balances.len() as u64 == num_leaves {
1200            Ok(RewardMerkleTreeV2Data { balances })
1201        } else {
1202            bail!(
1203                "RewardMerkleTreeV2 is incomplete, some accounts are missing. Balances length: \
1204                 {}, num_leaves: {num_leaves}.",
1205                balances.len(),
1206            );
1207        }
1208    }
1209}
1210
1211pub(crate) trait RewardMerkleTreeDataSource: Send + Sync + Clone + 'static {
1212    fn load_v1_reward_account_proof(
1213        &self,
1214        _height: u64,
1215        _account: RewardAccountV1,
1216    ) -> impl Send + Future<Output = anyhow::Result<RewardAccountQueryDataV1>>;
1217
1218    fn save_and_gc_reward_tree_v2(
1219        &self,
1220        node_state: &NodeState,
1221        height: u64,
1222        version: Version,
1223        merkle_tree: &RewardMerkleTreeV2,
1224    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1225        async move {
1226            self.save_reward_tree_v2(height, merkle_tree).await?;
1227
1228            // Skip garbage collection in tests
1229            if cfg!(any(test, feature = "testing")) {
1230                return Ok(());
1231            }
1232
1233            let finalized_hotshot_height = match node_state.finalized_hotshot_height().await {
1234                Ok(h) => h,
1235                Err(err) => {
1236                    tracing::warn!("failed to get finalized hotshot height: {err:#}");
1237                    return Ok(());
1238                },
1239            };
1240
1241            // trees at heights strictly less than the gc height are deleted
1242            //
1243            // keep recent epochs reward trees
1244            //   - staking-api-service at startup calls `reward-amounts` at
1245            //     `epoch_start - 1`, which needs the previous epoch's last-block
1246            //     tree on disk.
1247            //   - Per epoch reward (EPOCH_REWARD_VERSION+): `fetch_and_calculate`
1248            //     reads the previous epoch's last block tree to compute the next
1249            //     epoch's rewards.
1250            //
1251            // `finalized_hotshot_height`:  Reward claims
1252            //   (`reward-claim-input`) target the LightClient L1 finalization
1253            //   exactly.
1254
1255            let epoch_height = node_state
1256                .epoch_height
1257                .context("reward tree gc requires an epoch height")?;
1258            // EPOCH_REWARD_VERSION (V5)+ only persists a tree at each epoch boundary,
1259            // so 5 epochs = 5 trees on disk. Earlier versions persist a tree at
1260            // every block, so 1 epoch is already epoch_height trees; keeping more
1261            // would be expensive. We only need 1 epoch for both, but the extra
1262            // trees are cheap for V5+ so it doesn't make much of a difference.
1263            let epochs_to_retain = if version >= versions::EPOCH_REWARD_VERSION {
1264                5
1265            } else {
1266                1
1267            };
1268            let current_epoch = epoch_from_block_number(height, epoch_height);
1269            // First block of the oldest epoch we still want to retain.
1270            let epoch_start_block = current_epoch.saturating_sub(epochs_to_retain) * epoch_height;
1271
1272            let gc_height = epoch_start_block.min(finalized_hotshot_height);
1273
1274            if let Err(err) = self.garbage_collect(gc_height).await {
1275                tracing::info!(gc_height, "failed to garbage collect: {err:#}");
1276            }
1277
1278            Ok(())
1279        }
1280    }
1281
1282    /// Store `merkle_tree`, the whole reward tree at `height`, as key-value pairs.
1283    fn save_reward_tree_v2(
1284        &self,
1285        height: u64,
1286        merkle_tree: &RewardMerkleTreeV2,
1287    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1288        async move {
1289            // Every caller holds the whole tree: the merklized state loop always applies full
1290            // blocks, and the trees it starts from are whole. So an incomplete tree here indicates
1291            // something is seriously wrong.
1292            let tree_data = TryInto::<RewardMerkleTreeV2Data>::try_into(merkle_tree).inspect_err(
1293                |err| tracing::error!(%err, height, "cannot persist incomplete RewardMerkleTreeV2"),
1294            )?;
1295            let serialization =
1296                bincode::serialize(&tree_data).context("Merkle tree serialization failed")?;
1297            self.persist_tree(height, serialization).await
1298        }
1299    }
1300
1301    fn persist_reward_proofs(
1302        &self,
1303        node_state: &NodeState,
1304        height: u64,
1305        version: Version,
1306    ) -> impl Send + Future<Output = anyhow::Result<()>>;
1307
1308    fn load_reward_merkle_tree_v2(
1309        &self,
1310        height: u64,
1311    ) -> impl Send + Future<Output = anyhow::Result<PermittedRewardMerkleTreeV2>> {
1312        async move {
1313            let tree_bytes = self.load_tree(height).await?;
1314
1315            let tree_data = bincode::deserialize::<RewardMerkleTreeV2Data>(&tree_bytes).context(
1316                "Failed to deserialize RewardMerkleTreeV2 for height {height} from storage; this \
1317                 should never happen.",
1318            )?;
1319
1320            PermittedRewardMerkleTreeV2::try_from_kv_set(tree_data.balances)
1321                .await
1322                .context("Failed to reconstruct reward merkle tree from storage")
1323        }
1324    }
1325
1326    /// Returns the RewardMerkleTreeV2 for height <= requested height
1327    ///
1328    /// After V5 the tree is only written at epoch boundaries, so `reward_merkle_tree_v2_data`
1329    /// has no row for most heights. Within an epoch the tree doesn't change, so the previous
1330    /// boundary's tree matches the current block's reward root but only if we're actually in
1331    /// the same epoch. The caller is responsible for checking the returned tree's commitment
1332    /// against the header at `height`.
1333    /// if they differ we loaded a tree from an older epoch.
1334    fn load_latest_reward_merkle_tree_v2(
1335        &self,
1336        height: u64,
1337    ) -> impl Send + Future<Output = anyhow::Result<PermittedRewardMerkleTreeV2>> {
1338        async move {
1339            let tree_bytes = self.load_latest_tree(height).await?;
1340
1341            let tree_data = bincode::deserialize::<RewardMerkleTreeV2Data>(&tree_bytes)
1342                .context("Failed to deserialize RewardMerkleTreeV2 from storage")?;
1343
1344            PermittedRewardMerkleTreeV2::try_from_kv_set(tree_data.balances)
1345                .await
1346                .context("Failed to reconstruct reward merkle tree from storage")
1347        }
1348    }
1349
1350    fn load_reward_account_proof_v2(
1351        &self,
1352        _height: u64,
1353        _account: RewardAccountV2,
1354    ) -> impl Send + Future<Output = anyhow::Result<RewardAccountQueryDataV2>> {
1355        async {
1356            bail!("load_reward_account_proof_v2 is not supported for this data source");
1357        }
1358    }
1359
1360    fn load_latest_reward_account_proof_v2(
1361        &self,
1362        account: RewardAccountV2,
1363    ) -> impl Send + Future<Output = anyhow::Result<RewardAccountQueryDataV2>> {
1364        async move {
1365            let serialized_account = bincode::serialize(&account).context(
1366                "Failed to serialize RewardAccountV2 for lookup; this should never happen.",
1367            )?;
1368            let proof_bytes = self.load_latest_proof(serialized_account).await?;
1369
1370            bincode::deserialize::<RewardAccountQueryDataV2>(&proof_bytes).context(
1371                "Failed to deserialize RewardAccountQueryDataV2 for account {account} from \
1372                 storage; this should never happen.",
1373            )
1374        }
1375    }
1376
1377    fn persist_tree(
1378        &self,
1379        height: u64,
1380        merkle_tree: Vec<u8>,
1381    ) -> impl Send + Future<Output = anyhow::Result<()>>;
1382
1383    fn load_tree(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>>;
1384
1385    /// Load the latest serialized reward merkle tree v2 at height `<= height`.
1386    fn load_latest_tree(&self, height: u64)
1387    -> impl Send + Future<Output = anyhow::Result<Vec<u8>>>;
1388
1389    fn persist_proofs(
1390        &self,
1391        height: u64,
1392        proofs: impl Iterator<Item = (Vec<u8>, Vec<u8>)> + Send,
1393    ) -> impl Send + Future<Output = anyhow::Result<()>>;
1394
1395    fn load_proof(
1396        &self,
1397        height: u64,
1398        account: Vec<u8>,
1399        epoch_height: u64,
1400    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>>;
1401
1402    fn load_latest_proof(
1403        &self,
1404        account: Vec<u8>,
1405    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>>;
1406
1407    fn proof_exists(&self, height: u64) -> impl Send + Future<Output = bool>;
1408
1409    /// garbage collects merkle tree data for blocks strictly older than `height`
1410    fn garbage_collect(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<()>>;
1411}
1412
1413impl RewardMerkleTreeDataSource for hotshot_query_service::data_source::MetricsDataSource {
1414    fn load_v1_reward_account_proof(
1415        &self,
1416        _height: u64,
1417        _account: RewardAccountV1,
1418    ) -> impl Send + Future<Output = anyhow::Result<RewardAccountQueryDataV1>> {
1419        async {
1420            bail!("reward merklized state is not supported for this data source");
1421        }
1422    }
1423
1424    fn persist_reward_proofs(
1425        &self,
1426        _node_state: &NodeState,
1427        _height: u64,
1428        _version: Version,
1429    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1430        async {
1431            bail!("reward merklized state is not supported for this data source");
1432        }
1433    }
1434
1435    fn persist_tree(
1436        &self,
1437        _height: u64,
1438        _merkle_tree: Vec<u8>,
1439    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1440        async move {
1441            bail!("reward merklized state is not supported for this data source");
1442        }
1443    }
1444
1445    fn load_tree(&self, _height: u64) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1446        async move {
1447            bail!("reward merklized state is not supported for this data source");
1448        }
1449    }
1450
1451    fn load_latest_tree(
1452        &self,
1453        _height: u64,
1454    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1455        async move {
1456            bail!("reward merklized state is not supported for this data source");
1457        }
1458    }
1459
1460    fn garbage_collect(&self, _height: u64) -> impl Send + Future<Output = anyhow::Result<()>> {
1461        async move {
1462            bail!("reward merklized state is not supported for this data source");
1463        }
1464    }
1465
1466    fn persist_proofs(
1467        &self,
1468        _height: u64,
1469        _proofs: impl Iterator<Item = (Vec<u8>, Vec<u8>)> + Send,
1470    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1471        async move {
1472            bail!("reward merklized state is not supported for this data source");
1473        }
1474    }
1475
1476    fn load_proof(
1477        &self,
1478        _height: u64,
1479        _account: Vec<u8>,
1480        _epoch_height: u64,
1481    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1482        async move {
1483            bail!("reward merklized state is not supported for this data source");
1484        }
1485    }
1486
1487    fn load_latest_proof(
1488        &self,
1489        _account: Vec<u8>,
1490    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1491        async move {
1492            bail!("reward merklized state is not supported for this data source");
1493        }
1494    }
1495
1496    fn proof_exists(&self, _height: u64) -> impl Send + Future<Output = bool> {
1497        async move { false }
1498    }
1499}
1500
1501impl<T, S> RewardMerkleTreeDataSource
1502    for hotshot_query_service::data_source::ExtensibleDataSource<T, S>
1503where
1504    T: RewardMerkleTreeDataSource,
1505    S: Send + Sync + Clone + NodeStateDataSource + 'static,
1506{
1507    async fn load_v1_reward_account_proof(
1508        &self,
1509        height: u64,
1510        account: RewardAccountV1,
1511    ) -> anyhow::Result<RewardAccountQueryDataV1> {
1512        self.inner()
1513            .load_v1_reward_account_proof(height, account)
1514            .await
1515    }
1516
1517    async fn load_reward_account_proof_v2(
1518        &self,
1519        height: u64,
1520        account: RewardAccountV2,
1521    ) -> anyhow::Result<RewardAccountQueryDataV2> {
1522        let epoch_height = self
1523            .as_ref()
1524            .node_state()
1525            .await
1526            .epoch_height
1527            .context("epoch height not found")?;
1528        let serialized_account = bincode::serialize(&account)
1529            .context("Failed to serialize RewardAccountV2 for lookup; this should never happen.")?;
1530        let proof_bytes = self
1531            .inner()
1532            .load_proof(height, serialized_account, epoch_height)
1533            .await?;
1534
1535        bincode::deserialize::<RewardAccountQueryDataV2>(&proof_bytes)
1536            .context("Failed to deserialize RewardAccountQueryDataV2 from storage")
1537    }
1538
1539    fn persist_tree(
1540        &self,
1541        height: u64,
1542        merkle_tree: Vec<u8>,
1543    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1544        async move { self.inner().persist_tree(height, merkle_tree).await }
1545    }
1546
1547    fn load_tree(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1548        async move { self.inner().load_tree(height).await }
1549    }
1550
1551    fn load_latest_tree(
1552        &self,
1553        height: u64,
1554    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1555        async move { self.inner().load_latest_tree(height).await }
1556    }
1557
1558    fn garbage_collect(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<()>> {
1559        async move { self.inner().garbage_collect(height).await }
1560    }
1561
1562    fn persist_proofs(
1563        &self,
1564        height: u64,
1565        proofs: impl Iterator<Item = (Vec<u8>, Vec<u8>)> + Send,
1566    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1567        async move { self.inner().persist_proofs(height, proofs).await }
1568    }
1569
1570    fn load_proof(
1571        &self,
1572        height: u64,
1573        account: Vec<u8>,
1574        epoch_height: u64,
1575    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1576        async move { self.inner().load_proof(height, account, epoch_height).await }
1577    }
1578
1579    fn load_latest_proof(
1580        &self,
1581        account: Vec<u8>,
1582    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1583        async move { self.inner().load_latest_proof(account).await }
1584    }
1585
1586    fn proof_exists(&self, height: u64) -> impl Send + Future<Output = bool> {
1587        async move { self.inner().proof_exists(height).await }
1588    }
1589
1590    fn persist_reward_proofs(
1591        &self,
1592        node_state: &NodeState,
1593        height: u64,
1594        version: Version,
1595    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1596        async move {
1597            self.inner()
1598                .persist_reward_proofs(node_state, height, version)
1599                .await
1600        }
1601    }
1602}
1603
1604// Implement Reward MerkleTreeDataSource for Arc<D> to allow shared ownership
1605impl<D> RewardMerkleTreeDataSource for Arc<D>
1606where
1607    D: RewardMerkleTreeDataSource,
1608{
1609    async fn load_v1_reward_account_proof(
1610        &self,
1611        height: u64,
1612        account: RewardAccountV1,
1613    ) -> anyhow::Result<RewardAccountQueryDataV1> {
1614        (**self).load_v1_reward_account_proof(height, account).await
1615    }
1616
1617    fn persist_tree(
1618        &self,
1619        height: u64,
1620        merkle_tree: Vec<u8>,
1621    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1622        async move { (**self).persist_tree(height, merkle_tree).await }
1623    }
1624
1625    fn load_tree(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1626        async move { (**self).load_tree(height).await }
1627    }
1628
1629    fn load_latest_tree(
1630        &self,
1631        height: u64,
1632    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1633        async move { (**self).load_latest_tree(height).await }
1634    }
1635
1636    fn load_reward_merkle_tree_v2(
1637        &self,
1638        height: u64,
1639    ) -> impl Send + Future<Output = anyhow::Result<PermittedRewardMerkleTreeV2>> {
1640        async move { (**self).load_reward_merkle_tree_v2(height).await }
1641    }
1642
1643    fn load_reward_account_proof_v2(
1644        &self,
1645        height: u64,
1646        account: RewardAccountV2,
1647    ) -> impl Send + Future<Output = anyhow::Result<RewardAccountQueryDataV2>> {
1648        async move { (**self).load_reward_account_proof_v2(height, account).await }
1649    }
1650
1651    fn persist_proofs(
1652        &self,
1653        height: u64,
1654        proofs: impl Iterator<Item = (Vec<u8>, Vec<u8>)> + Send,
1655    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1656        async move { (**self).persist_proofs(height, proofs).await }
1657    }
1658
1659    fn persist_reward_proofs(
1660        &self,
1661        node_state: &NodeState,
1662        height: u64,
1663        version: Version,
1664    ) -> impl Send + Future<Output = anyhow::Result<()>> {
1665        async move {
1666            (**self)
1667                .persist_reward_proofs(node_state, height, version)
1668                .await
1669        }
1670    }
1671
1672    fn load_proof(
1673        &self,
1674        height: u64,
1675        account: Vec<u8>,
1676        epoch_height: u64,
1677    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1678        async move { (**self).load_proof(height, account, epoch_height).await }
1679    }
1680
1681    fn proof_exists(&self, height: u64) -> impl Send + Future<Output = bool> {
1682        async move { (**self).proof_exists(height).await }
1683    }
1684
1685    fn load_latest_proof(
1686        &self,
1687        account: Vec<u8>,
1688    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
1689        async move { (**self).load_latest_proof(account).await }
1690    }
1691
1692    fn garbage_collect(&self, height: u64) -> impl Send + Future<Output = anyhow::Result<()>> {
1693        async move { (**self).garbage_collect(height).await }
1694    }
1695}
1696
1697pub(crate) fn light_client_genesis(config: &NetworkConfig<SeqTypes>, chain_id: ChainId) -> Genesis {
1698    let epoch_height = config.config.epoch_height;
1699    let first_epoch = epoch_from_block_number(config.config.epoch_start_block, epoch_height);
1700    Genesis {
1701        epoch_height,
1702
1703        // Dynamic state starts from the third epoch, since we need the prior epoch's root
1704        // to have the upgraded header with the stake table hash.
1705        first_epoch_with_dynamic_stake_table: EpochNumber::new(first_epoch + 2),
1706
1707        stake_table: config
1708            .config
1709            .known_nodes_with_stake
1710            .iter()
1711            .map(|peer| peer.stake_table_entry.clone())
1712            .collect(),
1713
1714        chain_id,
1715    }
1716}
1717
1718/// [`Provider`] implementation wrapping a lazy [`LightClient`].
1719///
1720/// The [`LightClient`] requires a genesis to initialize itself, which we can get from the
1721/// [`ApiState`]. However, the [`Provider`] instance must be provided to the API data source at
1722/// initialization time, while the [`ApiState`] is only initialized lazily. This is a provider
1723/// implementation which is itself initialized lazily: [`Provider::fetch`] calls will time out until
1724/// the underlying [`ApiState`] is fully initialized, at which point this provider will start
1725/// serving fetches using the [`LightClient`].
1726///
1727/// A context that already runs a light client ([`ApiContext::light_client`]) shares it, so
1728/// fetches reuse its verified cache and no second database is opened on its path. The database
1729/// is therefore opened lazily as well; if that fails, the fetch returns `None` and the next fetch
1730/// tries again.
1731#[derive(Derivative)]
1732#[derivative(Debug(bound = ""))]
1733pub(crate) struct LightClientProvider {
1734    light_client: OnceCell<Arc<NodeLightClient>>,
1735    state: ApiState,
1736    client: FallbackClient<QueryServiceClient>,
1737    opt: LightClientOptions,
1738    db_opt: LightClientDbOptions,
1739}
1740
1741impl LightClientProvider {
1742    fn new(
1743        peers: impl IntoIterator<Item = Url>,
1744        state: ApiState,
1745        opt: LightClientOptions,
1746        db_opt: LightClientDbOptions,
1747    ) -> anyhow::Result<Self> {
1748        let client = FallbackClient::new(peers.into_iter().map(QueryServiceClient::new).collect())?;
1749        Ok(Self {
1750            light_client: OnceCell::new(),
1751            state,
1752            client,
1753            opt,
1754            db_opt,
1755        })
1756    }
1757
1758    pub(crate) async fn light_client(&self) -> Option<&Arc<NodeLightClient>> {
1759        self.light_client
1760            .get_or_try_init(|| self.init())
1761            .await
1762            .inspect_err(|err| {
1763                tracing::warn!(
1764                    "light client provider unavailable, retrying on the next fetch: {err:#}"
1765                )
1766            })
1767            .ok()
1768    }
1769
1770    async fn init(&self) -> anyhow::Result<Arc<NodeLightClient>> {
1771        let ctx = self.state.context().await;
1772        if let Some(light_client) = ctx.light_client() {
1773            return Ok(light_client);
1774        }
1775        let db = self
1776            .db_opt
1777            .clone()
1778            .connect()
1779            .await
1780            .context("opening the light client database")?;
1781        let chain_id = ctx.node_state().genesis_chain_config.chain_id;
1782        let genesis = light_client_genesis(&ctx.network_config(), chain_id);
1783        Ok(Arc::new(LightClient::from_genesis_with_options(
1784            db,
1785            self.client.clone(),
1786            genesis,
1787            self.opt.clone(),
1788        )))
1789    }
1790}
1791
1792#[async_trait]
1793impl<T> Provider<SeqTypes, T> for LightClientProvider
1794where
1795    T: fetching::Request<SeqTypes> + 'static,
1796    NodeLightClient: Provider<SeqTypes, T>,
1797{
1798    async fn fetch(&self, req: T) -> Option<T::Response> {
1799        self.light_client().await?.fetch(req).await
1800    }
1801}
1802
1803#[cfg(any(test, feature = "testing"))]
1804pub mod test_helpers {
1805    use std::{
1806        cmp::max,
1807        collections::HashSet,
1808        sync::atomic::{AtomicU32, Ordering},
1809        time::Duration,
1810    };
1811
1812    use alloy::{
1813        network::EthereumWallet,
1814        primitives::{Address, U256, utils::parse_ether},
1815        providers::{Provider, ProviderBuilder, ext::AnvilApi},
1816        signers::local::PrivateKeySigner,
1817    };
1818    use committable::{Commitment, Committable};
1819    use espresso_contract_deployer::{
1820        Contract, Contracts, DEFAULT_EXIT_ESCROW_PERIOD_SECONDS, builder::DeployerArgsBuilder,
1821        network_config::light_client_genesis_from_stake_table,
1822    };
1823    use espresso_types::{
1824        NamespaceId, TEST_UPGRADE, ValidatedState,
1825        v0::traits::{NullEventConsumer, PersistenceOptions, SequencerPersistence, StateCatchup},
1826    };
1827    use futures::{
1828        future::{FutureExt, join_all},
1829        stream::{Stream, StreamExt},
1830    };
1831    use hotshot_contract_adapter::stake_table::StakeTableContractVersion;
1832    use hotshot_types::{
1833        event::LeafInfo, light_client::LCV3StateSignatureRequestBody,
1834        new_protocol::CoordinatorEvent, traits::metrics::NoMetrics,
1835    };
1836    use http_client::{Client, error::ClientErr};
1837    use itertools::izip;
1838    use jf_merkle_tree_compat::{MerkleCommitment, MerkleTreeScheme};
1839    use staking_cli::{
1840        Transaction as StakingTransaction,
1841        demo::{DelegationConfig, StakingTransactions},
1842    };
1843    use tempfile::TempDir;
1844    use test_utils::reserve_tcp_port;
1845    use tokio::time::sleep;
1846    use vbs::version::StaticVersion;
1847    use versions::{EPOCH_VERSION, NEW_PROTOCOL_VERSION, Upgrade};
1848
1849    use super::*;
1850    use crate::{
1851        api::data_source::testing::TestableSequencerDataSource,
1852        catchup::{NullStateCatchup, ParallelStateCatchup, StatePeers},
1853        persistence::no_storage,
1854        testing::{
1855            TestConfig, TestConfigBuilder, decided_leaves, deploy_stake_table,
1856            wait_for_decide_on_handle, wait_for_epochs,
1857        },
1858    };
1859
1860    pub const STAKE_TABLE_CAPACITY_FOR_TEST: usize = 10;
1861
1862    /// One storage per node, and each node's persistence options on it.
1863    pub async fn node_storage<D: TestableSequencerDataSource, const NUM_NODES: usize>()
1864    -> (Vec<D::Storage>, [D::Options; NUM_NODES]) {
1865        let storage = join_all((0..NUM_NODES).map(|_| D::create_storage())).await;
1866        let persistence = std::array::from_fn(|i| D::persistence_options(&storage[i]));
1867        (storage, persistence)
1868    }
1869
1870    pub struct TestNetwork<P: PersistenceOptions, const NUM_NODES: usize> {
1871        pub server: SequencerContext<P::Persistence>,
1872        pub peers: Vec<SequencerContext<P::Persistence>>,
1873        pub cfg: TestConfig<{ NUM_NODES }>,
1874        // todo (abdul): remove this when fs storage is removed
1875        pub temp_dir: Option<TempDir>,
1876        pub contracts: Option<Contracts>,
1877        /// Deferred node indices not yet started (see [`Self::start_deferred_node`]).
1878        deferred: Vec<usize>,
1879        genesis_states: Vec<ValidatedState>,
1880        /// Node 0's API, if nodes use it for catchup (see [`with_api_catchup`]).
1881        api_catchup: Option<Url>,
1882    }
1883
1884    pub struct TestNetworkConfig<const NUM_NODES: usize, P, C>
1885    where
1886        P: PersistenceOptions,
1887        C: StateCatchup + 'static,
1888    {
1889        state: [ValidatedState; NUM_NODES],
1890        persistence: [P; NUM_NODES],
1891        catchup: [C; NUM_NODES],
1892        network_config: TestConfig<{ NUM_NODES }>,
1893        api_config: Options,
1894        contracts: Option<Contracts>,
1895        initial_token_supply: Option<U256>,
1896        deferred_start: Vec<usize>,
1897        api_catchup: bool,
1898    }
1899
1900    impl<const NUM_NODES: usize, P, C> TestNetworkConfig<{ NUM_NODES }, P, C>
1901    where
1902        P: PersistenceOptions,
1903        C: StateCatchup + 'static,
1904    {
1905        pub fn states(&self) -> [ValidatedState; NUM_NODES] {
1906            self.state.clone()
1907        }
1908
1909        fn has_stake_table(&self) -> bool {
1910            self.state[0]
1911                .chain_config
1912                .resolve()
1913                .is_some_and(|cf| cf.stake_table_contract.is_some())
1914        }
1915
1916        /// The new protocol reads every committee after genesis from the
1917        /// stake table contract, so a 0.6+ network needs one even when the
1918        /// test doesn't care about staking.
1919        async fn deploy_default_stake_table(&mut self) {
1920            let (state, contracts) = deploy_pos(
1921                &self.network_config,
1922                self.state[0].clone(),
1923                self.initial_token_supply,
1924                DelegationConfig::MultipleDelegators,
1925                StakeTableContractVersion::V3,
1926                &(0..NUM_NODES).collect::<Vec<_>>(),
1927            )
1928            .await
1929            .expect("failed to deploy the stake table");
1930            self.state = std::array::from_fn(|_| state.clone());
1931            self.contracts = Some(contracts);
1932        }
1933    }
1934
1935    #[derive(Clone)]
1936    pub struct TestNetworkConfigBuilder<const NUM_NODES: usize, P, C>
1937    where
1938        P: PersistenceOptions,
1939        C: StateCatchup + 'static,
1940    {
1941        state: [ValidatedState; NUM_NODES],
1942        persistence: Option<[P; NUM_NODES]>,
1943        catchup: Option<[C; NUM_NODES]>,
1944        api_config: Option<Options>,
1945        network_config: Option<TestConfig<{ NUM_NODES }>>,
1946        contracts: Option<Contracts>,
1947        initial_token_supply: Option<U256>,
1948        deferred_start: Vec<usize>,
1949        api_catchup: bool,
1950    }
1951
1952    impl Default for TestNetworkConfigBuilder<5, no_storage::Options, NullStateCatchup> {
1953        fn default() -> Self {
1954            TestNetworkConfigBuilder {
1955                state: std::array::from_fn(|_| ValidatedState::default()),
1956                persistence: Some([no_storage::Options; 5]),
1957                catchup: Some(std::array::from_fn(|_| NullStateCatchup::default())),
1958                network_config: None,
1959                api_config: None,
1960                contracts: None,
1961                initial_token_supply: None,
1962                deferred_start: Vec::new(),
1963                api_catchup: true,
1964            }
1965        }
1966    }
1967
1968    impl<const NUM_NODES: usize>
1969        TestNetworkConfigBuilder<{ NUM_NODES }, no_storage::Options, NullStateCatchup>
1970    {
1971        pub fn with_num_nodes()
1972        -> TestNetworkConfigBuilder<{ NUM_NODES }, no_storage::Options, NullStateCatchup> {
1973            TestNetworkConfigBuilder {
1974                state: std::array::from_fn(|_| ValidatedState::default()),
1975                persistence: Some([no_storage::Options; { NUM_NODES }]),
1976                catchup: Some(std::array::from_fn(|_| NullStateCatchup::default())),
1977                network_config: None,
1978                api_config: None,
1979                contracts: None,
1980                initial_token_supply: None,
1981                deferred_start: Vec::new(),
1982                api_catchup: true,
1983            }
1984        }
1985    }
1986
1987    impl<const NUM_NODES: usize, P, C> TestNetworkConfigBuilder<{ NUM_NODES }, P, C>
1988    where
1989        P: PersistenceOptions,
1990        C: StateCatchup + 'static,
1991    {
1992        pub fn states(mut self, state: [ValidatedState; NUM_NODES]) -> Self {
1993            self.state = state;
1994            self
1995        }
1996
1997        /// The chain config node 0 starts from.
1998        pub fn chain_config(&self) -> ChainConfig {
1999            self.state[0]
2000                .chain_config
2001                .resolve()
2002                .expect("node 0 starts from a full chain config")
2003        }
2004
2005        pub fn initial_token_supply(mut self, supply: U256) -> Self {
2006            self.initial_token_supply = Some(supply);
2007            self
2008        }
2009
2010        pub fn persistences<NP: PersistenceOptions>(
2011            self,
2012            persistence: [NP; NUM_NODES],
2013        ) -> TestNetworkConfigBuilder<{ NUM_NODES }, NP, C> {
2014            TestNetworkConfigBuilder {
2015                state: self.state,
2016                catchup: self.catchup,
2017                network_config: self.network_config,
2018                api_config: self.api_config,
2019                persistence: Some(persistence),
2020                contracts: self.contracts,
2021                initial_token_supply: self.initial_token_supply,
2022                deferred_start: self.deferred_start,
2023                api_catchup: self.api_catchup,
2024            }
2025        }
2026
2027        pub fn api_config(mut self, api_config: Options) -> Self {
2028            self.api_config = Some(api_config);
2029            self
2030        }
2031
2032        pub fn catchups<NC: StateCatchup + 'static>(
2033            self,
2034            catchup: [NC; NUM_NODES],
2035        ) -> TestNetworkConfigBuilder<{ NUM_NODES }, P, NC> {
2036            TestNetworkConfigBuilder {
2037                state: self.state,
2038                catchup: Some(catchup),
2039                network_config: self.network_config,
2040                api_config: self.api_config,
2041                persistence: self.persistence,
2042                contracts: self.contracts,
2043                initial_token_supply: self.initial_token_supply,
2044                deferred_start: self.deferred_start,
2045                api_catchup: self.api_catchup,
2046            }
2047        }
2048
2049        /// Defers starting the nodes at the given (trailing) indices; they
2050        /// join later via [`TestNetwork::start_deferred_node`].
2051        pub fn deferred_start(mut self, indices: &[usize]) -> Self {
2052            self.deferred_start = indices.to_vec();
2053            self
2054        }
2055
2056        /// Leaves node 0's API out of every node's catchup providers, for tests
2057        /// of a specific catchup route. A node then catches up only through
2058        /// its own provider, so past epoch 2 it needs one that serves leaves.
2059        pub fn without_api_catchup(mut self) -> Self {
2060            self.api_catchup = false;
2061            self
2062        }
2063
2064        pub fn network_config(mut self, network_config: TestConfig<{ NUM_NODES }>) -> Self {
2065            self.network_config = Some(network_config);
2066            self
2067        }
2068
2069        /// See [`TestConfig::set_genesis_chain_config`]. Call after `network_config`.
2070        pub fn genesis_chain_config(mut self, chain_config: ChainConfig) -> Self {
2071            self.network_config
2072                .as_mut()
2073                .expect("network_config is required")
2074                .set_genesis_chain_config(chain_config);
2075            self
2076        }
2077
2078        pub fn contracts(mut self, contracts: Contracts) -> Self {
2079            self.contracts = Some(contracts);
2080            self
2081        }
2082
2083        /// Setup for POS testing. Deploys contracts and adds the
2084        /// stake table address to state. Must be called before `build()`.
2085        pub async fn pos_hook(
2086            self,
2087            delegation_config: DelegationConfig,
2088            stake_table_version: StakeTableContractVersion,
2089            upgrade: Upgrade,
2090        ) -> anyhow::Result<Self> {
2091            let registered: Vec<usize> = (0..NUM_NODES).collect();
2092            self.pos_hook_with_registered(
2093                delegation_config,
2094                stake_table_version,
2095                upgrade,
2096                &registered,
2097            )
2098            .await
2099        }
2100
2101        /// Like [`Self::pos_hook`], but registers only the validators at the
2102        /// `registered` node indices on the stake table contract. The other
2103        /// nodes still run from genesis (which seeds the first two epochs)
2104        /// and can be registered mid-test via [`register_validators`].
2105        pub async fn pos_hook_with_registered(
2106            self,
2107            delegation_config: DelegationConfig,
2108            stake_table_version: StakeTableContractVersion,
2109            upgrade: Upgrade,
2110            registered: &[usize],
2111        ) -> anyhow::Result<Self> {
2112            if upgrade.base < EPOCH_VERSION && upgrade.target < EPOCH_VERSION {
2113                panic!("given version does not require pos deployment");
2114            };
2115
2116            let network_config = self
2117                .network_config
2118                .as_ref()
2119                .expect("network_config is required");
2120            let (state, contracts) = deploy_pos(
2121                network_config,
2122                self.state[0].clone(),
2123                self.initial_token_supply,
2124                delegation_config,
2125                stake_table_version,
2126                registered,
2127            )
2128            .await?;
2129            Ok(self
2130                .states(std::array::from_fn(|_| state.clone()))
2131                .contracts(contracts))
2132        }
2133
2134        pub fn build(self) -> TestNetworkConfig<{ NUM_NODES }, P, C> {
2135            TestNetworkConfig {
2136                state: self.state,
2137                persistence: self.persistence.unwrap(),
2138                catchup: self.catchup.unwrap(),
2139                network_config: self.network_config.unwrap(),
2140                api_config: self.api_config.unwrap(),
2141                contracts: self.contracts,
2142                initial_token_supply: self.initial_token_supply,
2143                deferred_start: self.deferred_start,
2144                api_catchup: self.api_catchup,
2145            }
2146        }
2147    }
2148
2149    impl<P: PersistenceOptions, const NUM_NODES: usize> TestNetwork<P, { NUM_NODES }> {
2150        pub async fn new<C: StateCatchup + 'static>(
2151            cfg: TestNetworkConfig<{ NUM_NODES }, P, C>,
2152            upgrade: versions::Upgrade,
2153        ) -> Self {
2154            let mut cfg = cfg;
2155            if upgrade.base >= NEW_PROTOCOL_VERSION && !cfg.has_stake_table() {
2156                cfg.deploy_default_stake_table().await;
2157            }
2158            // add default storage if none is provided as query module is now required
2159            let mut opt = cfg.api_config.clone();
2160            let temp_dir = if opt.storage_fs.is_none() && opt.storage_sql.is_none() {
2161                let temp_dir = tempfile::tempdir().unwrap();
2162                opt = opt.query_fs(
2163                    Default::default(),
2164                    crate::persistence::fs::Options::new(temp_dir.path().to_path_buf()),
2165                );
2166                Some(temp_dir)
2167            } else {
2168                None
2169            };
2170
2171            let deferred = cfg.deferred_start.clone();
2172            let genesis_states = cfg.state.to_vec();
2173            let api_catchup: Option<Url> = cfg.api_catchup.then(|| {
2174                format!("http://localhost:{}", opt.http.port)
2175                    .parse()
2176                    .unwrap()
2177            });
2178            assert!(
2179                deferred.len() < NUM_NODES,
2180                "node 0 runs the API server and cannot be deferred"
2181            );
2182            assert_eq!(
2183                deferred,
2184                (NUM_NODES - deferred.len()..NUM_NODES).collect::<Vec<_>>(),
2185                "deferred_start must be the trailing indices so `node(i)` stays aligned"
2186            );
2187
2188            let mut nodes = join_all(
2189                izip!(cfg.state, cfg.persistence, cfg.catchup)
2190                    .enumerate()
2191                    .filter(|(i, _)| !deferred.contains(i))
2192                    .map(|(i, (state, persistence, state_peers))| {
2193                        let state_peers = with_api_catchup(state_peers, api_catchup.as_ref());
2194                        let opt = opt.clone();
2195                        let cfg = &cfg.network_config;
2196                        let upgrades_map = cfg.upgrades();
2197                        async move {
2198                            if i == 0 {
2199                                opt.serve(|metrics, consumer, storage| {
2200                                    let cfg = cfg.clone();
2201                                    async move {
2202                                        Ok(cfg
2203                                            .init_node(
2204                                                0,
2205                                                state,
2206                                                persistence,
2207                                                Some(state_peers),
2208                                                storage,
2209                                                &*metrics,
2210                                                STAKE_TABLE_CAPACITY_FOR_TEST,
2211                                                consumer,
2212                                                upgrade,
2213                                                upgrades_map,
2214                                            )
2215                                            .await)
2216                                    }
2217                                    .boxed()
2218                                })
2219                                .await
2220                                .unwrap()
2221                            } else {
2222                                cfg.init_node(
2223                                    i,
2224                                    state,
2225                                    persistence,
2226                                    Some(state_peers),
2227                                    None,
2228                                    &NoMetrics,
2229                                    STAKE_TABLE_CAPACITY_FOR_TEST,
2230                                    NullEventConsumer,
2231                                    upgrade,
2232                                    upgrades_map,
2233                                )
2234                                .await
2235                            }
2236                        }
2237                        .boxed()
2238                    }),
2239            )
2240            .await;
2241
2242            for ctx in &nodes {
2243                ctx.start_consensus().await;
2244            }
2245
2246            let server = nodes.remove(0);
2247            let peers = nodes;
2248
2249            Self {
2250                server,
2251                peers,
2252                cfg: cfg.network_config,
2253                temp_dir,
2254                contracts: cfg.contracts,
2255                deferred,
2256                genesis_states,
2257                api_catchup,
2258            }
2259        }
2260
2261        /// Initializes and starts a node deferred at construction (see
2262        /// [`TestNetworkConfigBuilder::deferred_start`]), in ascending index
2263        /// order; the node is then reachable via [`Self::node`] as usual.
2264        pub async fn start_deferred_node<C: StateCatchup + 'static>(
2265            &mut self,
2266            i: usize,
2267            state: ValidatedState,
2268            persistence: P,
2269            catchup: C,
2270            upgrade: versions::Upgrade,
2271        ) -> &SequencerContext<P::Persistence> {
2272            assert_eq!(
2273                self.deferred.first(),
2274                Some(&i),
2275                "deferred nodes must be started in ascending index order"
2276            );
2277            self.deferred.remove(0);
2278
2279            let ctx = self
2280                .init_and_start(i, state, persistence, catchup, upgrade)
2281                .await;
2282            self.peers.push(ctx);
2283            self.peers.last().unwrap()
2284        }
2285
2286        /// Shuts the node at index `i` down and reinitializes it from the
2287        /// network's current configuration, picking up any rotated consensus
2288        /// keys or coordinator address (see [`TestConfig::set_consensus_keys`]
2289        /// and [`TestConfig::set_coordinator_addr`]). Node 0 hosts the query
2290        /// API and cannot be restarted this way.
2291        pub async fn restart_node<C: StateCatchup + 'static>(
2292            &mut self,
2293            i: usize,
2294            state: ValidatedState,
2295            persistence: P,
2296            catchup: C,
2297            upgrade: versions::Upgrade,
2298        ) -> &SequencerContext<P::Persistence> {
2299            self.stop_node(i).await;
2300            self.start_stopped_node(i, state, persistence, catchup, upgrade)
2301                .await
2302        }
2303
2304        /// Shuts the node at index `i` down, leaving it stopped until
2305        /// [`Self::start_stopped_node`]. Node 0 hosts the query API and cannot
2306        /// be stopped this way.
2307        pub async fn stop_node(&mut self, i: usize) {
2308            assert_ne!(i, 0, "node 0 runs the API server and cannot be restarted");
2309            assert!(
2310                !self.deferred.contains(&i),
2311                "node {i} was deferred and has not been started yet"
2312            );
2313            self.peers[i - 1].shut_down().await;
2314            // The restarted node may rebind the very coordinator port it
2315            // just released, but `shut_down` cannot await the listener drop:
2316            // aborted tasks holding network senders keep it alive. Poll
2317            // until the address is actually bindable again.
2318            let addr = self.cfg.coordinator_addr(i).to_string();
2319            timeout(Duration::from_secs(60), async {
2320                while std::net::TcpListener::bind(&addr).is_err() {
2321                    sleep(Duration::from_millis(100)).await;
2322                }
2323            })
2324            .await
2325            .expect("shut-down node did not release its coordinator port");
2326        }
2327
2328        /// Reinitializes a node shut down by [`Self::stop_node`] from the
2329        /// network's current configuration and starts consensus on it.
2330        pub async fn start_stopped_node<C: StateCatchup + 'static>(
2331            &mut self,
2332            i: usize,
2333            state: ValidatedState,
2334            persistence: P,
2335            catchup: C,
2336            upgrade: versions::Upgrade,
2337        ) -> &SequencerContext<P::Persistence> {
2338            let ctx = self
2339                .init_and_start(i, state, persistence, catchup, upgrade)
2340                .await;
2341            self.peers[i - 1] = ctx;
2342            &self.peers[i - 1]
2343        }
2344
2345        /// The state node `i` started from, for bringing it back from genesis.
2346        pub fn genesis_state(&self, i: usize) -> ValidatedState {
2347            self.genesis_states[i].clone()
2348        }
2349
2350        /// Initializes node `i` from the network's current configuration and
2351        /// starts consensus on it, the same way construction does.
2352        async fn init_and_start<C: StateCatchup + 'static>(
2353            &self,
2354            i: usize,
2355            state: ValidatedState,
2356            persistence: P,
2357            catchup: C,
2358            upgrade: versions::Upgrade,
2359        ) -> SequencerContext<P::Persistence> {
2360            let ctx = self
2361                .cfg
2362                .init_node(
2363                    i,
2364                    state,
2365                    persistence,
2366                    Some(with_api_catchup(catchup, self.api_catchup.as_ref())),
2367                    None,
2368                    &NoMetrics,
2369                    STAKE_TABLE_CAPACITY_FOR_TEST,
2370                    NullEventConsumer,
2371                    upgrade,
2372                    self.cfg.upgrades(),
2373                )
2374                .await;
2375            ctx.start_consensus().await;
2376            ctx
2377        }
2378
2379        pub async fn stop_consensus(&mut self) {
2380            self.server.shutdown_consensus().await;
2381
2382            for ctx in &mut self.peers {
2383                ctx.shutdown_consensus().await;
2384            }
2385        }
2386
2387        /// The context of the node at index `i` (node 0 is the API server).
2388        pub fn node(&self, i: usize) -> &SequencerContext<P::Persistence> {
2389            if i == 0 {
2390                &self.server
2391            } else {
2392                &self.peers[i - 1]
2393            }
2394        }
2395    }
2396
2397    /// Adds node 0's API, if given, as a catchup provider alongside `catchup`. From epoch 3 the
2398    /// new protocol fetches epoch root leaves through catchup, so a node without storage or state
2399    /// peers would otherwise wedge at the first epoch boundary. Both providers are queried at
2400    /// once, so a test of `catchup` itself must leave the API out.
2401    fn with_api_catchup(
2402        catchup: impl StateCatchup + 'static,
2403        api_url: Option<&Url>,
2404    ) -> ParallelStateCatchup {
2405        let mut providers: Vec<Arc<dyn StateCatchup>> = vec![Arc::new(catchup)];
2406        if let Some(url) = api_url {
2407            providers.push(Arc::new(StatePeers::<SequencerApiVersion>::from_urls(
2408                vec![url.clone()],
2409                Default::default(),
2410                Duration::from_secs(2),
2411                &NoMetrics,
2412            )));
2413        }
2414        ParallelStateCatchup::new(&providers, Duration::from_secs(5))
2415    }
2416
2417    /// Deploys the stake table (with a mock light client) sized to the
2418    /// network's epochs, registers the validators at `registered`, and
2419    /// returns `state` with the contract in its chain config and a zero base
2420    /// fee, so no test pays for `FeeState` catchup.
2421    async fn deploy_pos<const NUM_NODES: usize>(
2422        network_config: &TestConfig<NUM_NODES>,
2423        state: ValidatedState,
2424        initial_token_supply: Option<U256>,
2425        delegation_config: DelegationConfig,
2426        stake_table_version: StakeTableContractVersion,
2427        registered: &[usize],
2428    ) -> anyhow::Result<(ValidatedState, Contracts)> {
2429        let l1_url = network_config.l1_url();
2430        let signer = network_config.signer();
2431        let deployer = ProviderBuilder::new()
2432            .wallet(EthereumWallet::from(signer.clone()))
2433            .connect_http(l1_url.clone());
2434
2435        let blocks_per_epoch = network_config.hotshot_config().epoch_height;
2436        let epoch_start_block = network_config.hotshot_config().epoch_start_block;
2437        let (genesis_state, genesis_stake) = light_client_genesis_from_stake_table(
2438            &network_config.hotshot_config().hotshot_stake_table(),
2439            STAKE_TABLE_CAPACITY_FOR_TEST,
2440        )
2441        .unwrap();
2442
2443        let mut contracts = Contracts::new();
2444        let args = DeployerArgsBuilder::default()
2445            .deployer(deployer.clone())
2446            .rpc_url(l1_url.clone())
2447            .mock_light_client(true)
2448            .genesis_lc_state(genesis_state)
2449            .genesis_st_state(genesis_stake)
2450            .blocks_per_epoch(blocks_per_epoch)
2451            .epoch_start_block(epoch_start_block)
2452            .exit_escrow_period(U256::from(max(
2453                blocks_per_epoch * 15 + 100,
2454                DEFAULT_EXIT_ESCROW_PERIOD_SECONDS,
2455            )))
2456            .multisig_pauser(signer.address())
2457            .token_name("Espresso".to_string())
2458            .token_symbol("ESP".to_string())
2459            .initial_token_supply(initial_token_supply.unwrap_or(U256::from(100000u64)))
2460            .ops_timelock_delay(U256::from(0))
2461            .ops_timelock_admin(signer.address())
2462            .ops_timelock_proposers(vec![signer.address()])
2463            .ops_timelock_executors(vec![signer.address()])
2464            .safe_exit_timelock_delay(U256::from(10))
2465            .safe_exit_timelock_admin(signer.address())
2466            .safe_exit_timelock_proposers(vec![signer.address()])
2467            .safe_exit_timelock_executors(vec![signer.address()])
2468            .build()
2469            .unwrap();
2470
2471        deploy_stake_table(&args, stake_table_version, &mut contracts)
2472            .await
2473            .context("failed to deploy contracts")?;
2474
2475        let stake_table_address = contracts
2476            .address(Contract::StakeTableProxy)
2477            .expect("StakeTableProxy address not found");
2478
2479        StakingTransactions::create(
2480            l1_url.clone(),
2481            &deployer,
2482            stake_table_address,
2483            network_config.staking_key_sets(registered),
2484            None,
2485            delegation_config,
2486        )
2487        .await
2488        .expect("stake table setup failed")
2489        .apply_all()
2490        .await
2491        .expect("send all txns failed");
2492
2493        // Stake table updates are read at the finalized L1 block, so keep L1
2494        // finalizing even when no transactions are sent.
2495        if let Some(anvil) = network_config.anvil() {
2496            anvil
2497                .anvil_set_interval_mining(1)
2498                .await
2499                .expect("interval mining");
2500        }
2501
2502        let chain_config = ChainConfig {
2503            base_fee: 0.into(),
2504            stake_table_contract: Some(stake_table_address),
2505            ..state.chain_config.resolve().unwrap_or_default()
2506        };
2507        let state = ValidatedState {
2508            chain_config: chain_config.into(),
2509            ..state
2510        };
2511        Ok((state, contracts))
2512    }
2513
2514    /// Registers and delegates to a batch of new validators mid-run, funding
2515    /// their L1 accounts from the network's deployer signer.
2516    pub async fn register_validators<const NUM_NODES: usize>(
2517        cfg: &TestConfig<NUM_NODES>,
2518        stake_table: Address,
2519        indices: &[usize],
2520        delegation_config: DelegationConfig,
2521    ) -> anyhow::Result<()> {
2522        let deployer = ProviderBuilder::new()
2523            .wallet(EthereumWallet::from(cfg.signer()))
2524            .connect_http(cfg.l1_url());
2525        StakingTransactions::create(
2526            cfg.l1_url(),
2527            &deployer,
2528            stake_table,
2529            cfg.staking_key_sets(indices),
2530            None,
2531            delegation_config,
2532        )
2533        .await?
2534        .apply_all()
2535        .await?;
2536        Ok(())
2537    }
2538
2539    /// Deregisters the validators at the given node indices, each exit sent
2540    /// from that validator's own funded provider.
2541    pub async fn deregister_validators<const NUM_NODES: usize>(
2542        cfg: &TestConfig<NUM_NODES>,
2543        stake_table: Address,
2544        indices: &[usize],
2545    ) -> anyhow::Result<()> {
2546        let providers = cfg.validator_providers();
2547        for &i in indices {
2548            let (address, provider) = &providers[i];
2549            let receipt = StakingTransaction::DeregisterValidator { stake_table }
2550                .send(provider)
2551                .await?
2552                .get_receipt()
2553                .await?;
2554            anyhow::ensure!(
2555                receipt.status(),
2556                "deregistration of validator {i} ({address}) reverted"
2557            );
2558        }
2559        Ok(())
2560    }
2561
2562    /// Funds a fresh delegator account (ETH via anvil, ESP from the deployer)
2563    /// and delegates `amount` to `validator`. Returns the delegator's provider
2564    /// so the test can later undelegate.
2565    pub async fn delegate_new<const NUM_NODES: usize>(
2566        cfg: &TestConfig<NUM_NODES>,
2567        token: Address,
2568        stake_table: Address,
2569        validator: Address,
2570        amount: U256,
2571    ) -> anyhow::Result<impl Provider + Clone + use<NUM_NODES>> {
2572        let deployer = ProviderBuilder::new()
2573            .wallet(EthereumWallet::from(cfg.signer()))
2574            .connect_http(cfg.l1_url());
2575        let signer = PrivateKeySigner::random();
2576        let delegator = signer.address();
2577        let provider = ProviderBuilder::new()
2578            .wallet(EthereumWallet::from(signer))
2579            .connect_http(cfg.l1_url());
2580
2581        deployer
2582            .anvil_set_balance(delegator, parse_ether("10").unwrap())
2583            .await?;
2584        let funding = StakingTransaction::Transfer {
2585            token,
2586            to: delegator,
2587            amount,
2588        }
2589        .send(&deployer)
2590        .await?
2591        .get_receipt()
2592        .await?;
2593        anyhow::ensure!(funding.status(), "ESP transfer to delegator reverted");
2594
2595        for tx in [
2596            StakingTransaction::Approve {
2597                token,
2598                spender: stake_table,
2599                amount,
2600            },
2601            StakingTransaction::Delegate {
2602                stake_table,
2603                validator,
2604                amount,
2605            },
2606        ] {
2607            let receipt = tx.send(&provider).await?.get_receipt().await?;
2608            anyhow::ensure!(receipt.status(), "delegator transaction reverted");
2609        }
2610        Ok(provider)
2611    }
2612
2613    /// Waits epoch by epoch, starting at `start_epoch`, until the committee
2614    /// reported by `node/validators/{epoch}` satisfies `pred`. Returns the
2615    /// first matching epoch and its committee; panics after `max_epochs`
2616    /// epochs without a match.
2617    pub async fn wait_for_committee(
2618        client: &Client<ClientErr, SequencerApiVersion>,
2619        events: &mut (impl Stream<Item = CoordinatorEvent<SeqTypes>> + Unpin),
2620        epoch_height: u64,
2621        start_epoch: u64,
2622        max_epochs: u64,
2623        pred: impl Fn(&AuthenticatedValidatorMap) -> bool,
2624    ) -> (u64, AuthenticatedValidatorMap) {
2625        let mut last = None;
2626        for epoch in start_epoch..start_epoch + max_epochs {
2627            wait_for_epochs(events, epoch_height, epoch).await;
2628            let validators = client
2629                .get::<AuthenticatedValidatorMap>(&format!("node/validators/{epoch}"))
2630                .send()
2631                .await
2632                .expect("validators for a decided epoch");
2633            if pred(&validators) {
2634                return (epoch, validators);
2635            }
2636            last = Some((epoch, validators));
2637        }
2638        let last =
2639            last.map(|(epoch, validators)| (epoch, validators.keys().copied().collect::<Vec<_>>()));
2640        panic!(
2641            "committee predicate not satisfied within {max_epochs} epochs starting at \
2642             {start_epoch}; last committee: {last:?}"
2643        );
2644    }
2645
2646    /// The L1 accounts of the validators at the given node indices.
2647    pub fn staking_addresses<const NUM_NODES: usize>(
2648        cfg: &TestConfig<NUM_NODES>,
2649        indices: &[usize],
2650    ) -> HashSet<Address> {
2651        cfg.staking_key_sets(indices)
2652            .iter()
2653            .map(|keys| keys.signer.address())
2654            .collect()
2655    }
2656
2657    /// Predicate for [`wait_for_committee`]: the committee is exactly the
2658    /// expected set of validator accounts.
2659    pub fn committee_is(expected: HashSet<Address>) -> impl Fn(&AuthenticatedValidatorMap) -> bool {
2660        move |validators| validators.keys().copied().collect::<HashSet<_>>() == expected
2661    }
2662
2663    /// Asserts the node is live: it must advance `epochs_ahead` epochs (at
2664    /// least 1) past its current decided epoch, and, when the chain runs the
2665    /// self-building new protocol, sequence a newly submitted transaction.
2666    /// Inclusion is not asserted on legacy versions because the test-only
2667    /// legacy builder stops producing non-empty blocks after roughly a
2668    /// hundred views, independent of any stake table activity.
2669    pub async fn assert_node_live<P: SequencerPersistence>(
2670        node: &SequencerContext<P>,
2671        epoch_height: u64,
2672        epochs_ahead: u64,
2673    ) {
2674        assert!(epochs_ahead > 0, "epochs_ahead must be at least 1");
2675        let mut events = node.event_stream();
2676        let leaf = node.decided_leaf().await;
2677        let current = leaf
2678            .epoch(epoch_height)
2679            .map(|epoch| epoch.u64())
2680            .unwrap_or_default();
2681        // `wait_for_epochs` returns on the first epoch strictly greater than
2682        // its target.
2683        wait_for_epochs(&mut events, epoch_height, current + epochs_ahead - 1).await;
2684
2685        if node.decided_leaf().await.block_header().version() < versions::NEW_PROTOCOL_VERSION {
2686            tracing::info!("legacy version: skipping transaction-inclusion liveness check");
2687            return;
2688        }
2689        // Detect inclusion via the header's namespace table: the namespace is
2690        // unique to this call, and decide events at 0.6 do not always carry
2691        // payloads.
2692        static NAMESPACE_COUNTER: AtomicU32 = AtomicU32::new(10_101);
2693        let namespace = NamespaceId::from(NAMESPACE_COUNTER.fetch_add(1, Ordering::Relaxed));
2694        let tx = Transaction::new(namespace, vec![7; 8]);
2695        node.submit_transaction(tx)
2696            .await
2697            .expect("live node accepts transactions");
2698        tokio::time::timeout(Duration::from_secs(120), async {
2699            loop {
2700                let leaf = match events.next().await.unwrap() {
2701                    CoordinatorEvent::NewDecide { leaf_infos, .. } => leaf_infos[0].leaf.clone(),
2702                    _ => continue,
2703                };
2704                if leaf
2705                    .block_header()
2706                    .ns_table()
2707                    .find_ns_id(&namespace)
2708                    .is_some()
2709                {
2710                    tracing::info!(height = leaf.height(), "transaction namespace sequenced");
2711                    return;
2712                }
2713            }
2714        })
2715        .await
2716        .expect("submitted transaction was not sequenced in time");
2717    }
2718
2719    /// Asserts every node has decided at least `min_height`, and that nodes
2720    /// which have decided the same height agree on the leaf.
2721    pub async fn assert_nodes_agree<P: SequencerPersistence>(
2722        nodes: &[&SequencerContext<P>],
2723        min_height: u64,
2724    ) {
2725        let leaves = join_all(nodes.iter().map(|node| node.decided_leaf())).await;
2726        let mut by_height: std::collections::BTreeMap<u64, Commitment<Leaf2>> = Default::default();
2727        for (i, leaf) in leaves.iter().enumerate() {
2728            assert!(
2729                leaf.height() >= min_height,
2730                "node {i} decided height {} is below {min_height}",
2731                leaf.height()
2732            );
2733            if let Some(other) = by_height.insert(leaf.height(), leaf.commit()) {
2734                assert_eq!(
2735                    other,
2736                    leaf.commit(),
2737                    "decided-leaf divergence at height {}",
2738                    leaf.height()
2739                );
2740            }
2741        }
2742    }
2743
2744    /// Test the status API with custom options.
2745    ///
2746    /// The `opt` function can be used to modify the [`Options`] which are used to start the server.
2747    /// By default, the options are the minimal required to run this test (configuring a port and
2748    /// enabling the status API). `opt` may add additional functionality (e.g. adding a query module
2749    /// to test a different initialization path) but should not remove or modify the existing
2750    /// functionality (e.g. removing the status module or changing the port).
2751    pub async fn status_test_helper(opt: impl FnOnce(Options) -> Options) {
2752        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
2753        let url = format!("http://localhost:{port}").parse().unwrap();
2754        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
2755
2756        let options = opt(Options::with_port(port));
2757        let network_config = TestConfigBuilder::default().build();
2758        let config = TestNetworkConfigBuilder::default()
2759            .api_config(options)
2760            .network_config(network_config)
2761            .build();
2762        let network = TestNetwork::new(config, TEST_UPGRADE).await;
2763        client.connect(None).await;
2764
2765        // The status API is well tested in the query service repo. Here we are just smoke testing
2766        // that we set it up correctly. Wait for a (non-genesis) block to be sequenced and then
2767        // check the success rate metrics.
2768        while client
2769            .get::<u64>("status/block-height")
2770            .send()
2771            .await
2772            .unwrap()
2773            <= 1
2774        {
2775            sleep(Duration::from_secs(1)).await;
2776        }
2777        let success_rate = client
2778            .get::<f64>("status/success-rate")
2779            .send()
2780            .await
2781            .unwrap();
2782        // If metrics are populating correctly, we should get a finite number. If not, we might get
2783        // NaN or infinity due to division by 0.
2784        assert!(success_rate.is_finite(), "{success_rate}");
2785        // We know at least some views have been successful, since we finalized a block.
2786        assert!(success_rate > 0.0, "{success_rate}");
2787
2788        let keys: NodePublicKeys = client.get("status/keys").send().await.unwrap();
2789        let expected = network.server.validator_config();
2790        assert_eq!(keys.consensus_key, expected.public_key);
2791        assert_eq!(keys.state_ver_key, expected.state_public_key);
2792        assert_eq!(
2793            keys.x25519_key,
2794            expected.x25519_keypair.as_ref().map(|kp| kp.public_key())
2795        );
2796        assert_eq!(keys.p2p_addr, expected.p2p_addr);
2797        assert!(keys.p2p_addr.is_some());
2798        assert_eq!(keys.eth_account, None);
2799
2800        let json: serde_json::Value = client.get("status/keys").send().await.unwrap();
2801        let bls = json["consensus_key"].as_str().unwrap();
2802        assert!(bls.starts_with("BLS_VER_KEY~"), "{bls}");
2803        let schnorr = json["state_ver_key"].as_str().unwrap();
2804        assert!(schnorr.starts_with("SCHNORR_VER_KEY~"), "{schnorr}");
2805        let x25519 = json["x25519_key"].as_str().unwrap();
2806        assert!(x25519.starts_with("X25519_PK~"), "{x25519}");
2807        assert_eq!(
2808            json["p2p_addr"].as_str().unwrap(),
2809            expected.p2p_addr.as_ref().unwrap().to_string()
2810        );
2811    }
2812
2813    /// Test the submit API with custom options.
2814    ///
2815    /// The `opt` function can be used to modify the [`Options`] which are used to start the server.
2816    /// By default, the options are the minimal required to run this test (configuring a port and
2817    /// enabling the submit API). `opt` may add additional functionality (e.g. adding a query module
2818    /// to test a different initialization path) but should not remove or modify the existing
2819    /// functionality (e.g. removing the submit module or changing the port).
2820    pub async fn submit_test_helper(opt: impl FnOnce(Options) -> Options) {
2821        let txn = Transaction::new(NamespaceId::from(1_u32), vec![1, 2, 3, 4]);
2822
2823        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
2824
2825        let url = format!("http://localhost:{port}").parse().unwrap();
2826        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
2827
2828        let options = opt(Options::with_port(port).submit(Default::default()));
2829        let network_config = TestConfigBuilder::default().build();
2830        let config = TestNetworkConfigBuilder::default()
2831            .api_config(options)
2832            .network_config(network_config)
2833            .build();
2834        let network = TestNetwork::new(config, TEST_UPGRADE).await;
2835        let mut events = network.server.event_stream();
2836
2837        client.connect(None).await;
2838
2839        let hash = client
2840            .post("submit/submit")
2841            .body_json(&txn)
2842            .unwrap()
2843            .send()
2844            .await
2845            .unwrap();
2846        assert_eq!(txn.commit(), hash);
2847
2848        // Wait for a Decide event containing transaction matching the one we sent
2849        wait_for_decide_on_handle(&mut events, &txn).await;
2850    }
2851
2852    /// Test the state signature API.
2853    pub async fn state_signature_test_helper(opt: impl FnOnce(Options) -> Options) {
2854        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
2855
2856        let url = format!("http://localhost:{port}").parse().unwrap();
2857
2858        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
2859
2860        let options = opt(Options::with_port(port));
2861        let network_config = TestConfigBuilder::default().build();
2862        let config = TestNetworkConfigBuilder::default()
2863            .api_config(options)
2864            .network_config(network_config)
2865            .build();
2866        let network = TestNetwork::new(config, TEST_UPGRADE).await;
2867
2868        let mut height: u64;
2869        // Wait for block >=2 appears
2870        // It's waiting for an extra second to make sure that the signature is generated
2871        loop {
2872            height = network.server.decided_leaf().await.height();
2873            sleep(std::time::Duration::from_secs(1)).await;
2874            if height >= 2 {
2875                break;
2876            }
2877        }
2878        // we cannot verify the signature now, because we don't know the stake table
2879        client
2880            .get::<LCV3StateSignatureRequestBody>(&format!("state-signature/block/{height}"))
2881            .send()
2882            .await
2883            .unwrap();
2884    }
2885
2886    /// Test the catchup API with custom options.
2887    ///
2888    /// The `opt` function can be used to modify the [`Options`] which are used to start the server.
2889    /// By default, the options are the minimal required to run this test (configuring a port and
2890    /// enabling the catchup API). `opt` may add additional functionality (e.g. adding a query module
2891    /// to test a different initialization path) but should not remove or modify the existing
2892    /// functionality (e.g. removing the catchup module or changing the port).
2893    pub async fn catchup_test_helper(opt: impl FnOnce(Options) -> Options) {
2894        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
2895        let url = format!("http://localhost:{port}").parse().unwrap();
2896        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
2897
2898        let options = opt(Options::with_port(port));
2899        let network_config = TestConfigBuilder::default().build();
2900        let config = TestNetworkConfigBuilder::default()
2901            .api_config(options)
2902            .network_config(network_config)
2903            .build();
2904        let network = TestNetwork::new(config, TEST_UPGRADE).await;
2905        client.connect(None).await;
2906
2907        // Wait for a few blocks to be decided.
2908        let mut events = network.server.event_stream();
2909        loop {
2910            if let Some(leaf_chain) = decided_leaves(&events.next().await.unwrap())
2911                && leaf_chain
2912                    .iter()
2913                    .any(|LeafInfo { leaf, .. }| leaf.block_header().height() > 2)
2914            {
2915                break;
2916            }
2917        }
2918
2919        // Consensus keeps running: at 0.6 stopping it drops every in-memory state. So the
2920        // leaf can be decided and garbage collected between reading its state and serving the
2921        // request; retry on a fresh undecided leaf when that happens.
2922        for attempt in 0.. {
2923            assert!(attempt < 50, "never served catchup for an undecided state");
2924            if try_undecided_catchup(&network, &client).await {
2925                return;
2926            }
2927            sleep(Duration::from_millis(100)).await;
2928        }
2929    }
2930
2931    /// Checks the catchup API against the newest undecided state, or returns `false` if there
2932    /// is none or it went out of memory mid-check.
2933    async fn try_undecided_catchup<P: PersistenceOptions, const NUM_NODES: usize>(
2934        network: &TestNetwork<P, NUM_NODES>,
2935        client: &Client<ClientErr, StaticVersion<0, 1>>,
2936    ) -> bool {
2937        let undecided = network.server.consensus_handle().undecided_leaves().await;
2938        let Some(leaf) = undecided.iter().max_by_key(|leaf| leaf.height()) else {
2939            return false;
2940        };
2941        let (height, view) = (leaf.height(), leaf.view_number());
2942        let Some(state) = network.server.state(view).await else {
2943            return false;
2944        };
2945
2946        // Undecided fee state: absent account.
2947        let Ok(res) = client
2948            .get::<AccountQueryData>(&format!(
2949                "catchup/{height}/{}/account/{:x}",
2950                view.u64(),
2951                Address::default()
2952            ))
2953            .send()
2954            .await
2955        else {
2956            return false;
2957        };
2958        assert_eq!(res.balance, U256::ZERO);
2959        assert_eq!(
2960            res.proof
2961                .verify(&state.fee_merkle_tree.commitment())
2962                .unwrap(),
2963            U256::ZERO,
2964        );
2965
2966        // Undecided block state.
2967        let Ok(res) = client
2968            .get::<BlocksFrontier>(&format!("catchup/{height}/{}/blocks", view.u64()))
2969            .send()
2970            .await
2971        else {
2972            return false;
2973        };
2974        let root = &state.block_merkle_tree.commitment();
2975        BlockMerkleTree::verify(root, root.size() - 1, res)
2976            .unwrap()
2977            .unwrap();
2978        true
2979    }
2980}
2981
2982#[cfg(test)]
2983mod api_tests {
2984    use std::{fmt::Debug, marker::PhantomData};
2985
2986    use committable::Committable;
2987    use data_source::testing::TestableSequencerDataSource;
2988    use espresso_types::{
2989        Header, Leaf2, NamespaceId, NamespaceProofQueryData, TEST_UPGRADE, ValidatedState,
2990        traits::{EventConsumer, PersistenceOptions},
2991    };
2992    use futures::{future, stream::StreamExt};
2993    use hotshot_query_service::availability::{
2994        AvailabilityDataSource, BlockQueryData, VidCommonQueryData,
2995    };
2996    use hotshot_types::{
2997        data::{
2998            DaProposal2, EpochNumber, QuorumProposal2, QuorumProposalWrapper, VidCommitment,
2999            VidDisperseShare, ns_table::parse_ns_table, vid_disperse::AvidmGf2DisperseShare,
3000        },
3001        event::LeafInfo,
3002        message::Proposal,
3003        simple_certificate::{CertificatePair, QuorumCertificate2},
3004        traits::{EncodeBytes, signature_key::SignatureKey},
3005        utils::EpochTransitionIndicator,
3006        vid::avidm_gf2::{AvidmGf2Scheme, init_avidm_gf2_param},
3007    };
3008    use http_client::{Client, error::ClientErr};
3009    use test_helpers::{
3010        TestNetwork, TestNetworkConfigBuilder, catchup_test_helper, state_signature_test_helper,
3011        status_test_helper, submit_test_helper,
3012    };
3013    use test_utils::reserve_tcp_port;
3014    use vbs::version::StaticVersion;
3015    use versions::NEW_PROTOCOL_VERSION;
3016
3017    use super::{update::ApiEventConsumer, *};
3018    use crate::{
3019        persistence::sql::DECIDE_GAP_FILL_HORIZON,
3020        testing::{TestConfigBuilder, wait_for_decide_on_handle},
3021    };
3022
3023    #[rstest_reuse::template]
3024    #[rstest::rstest]
3025    #[case::postgres(PhantomData::<crate::api::sql::postgres::DataSource>)]
3026    #[case::sqlite(PhantomData::<crate::api::sql::sqlite::DataSource>)]
3027    #[case::fs(PhantomData::<crate::api::fs::DataSource>)]
3028    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3029    pub fn testable_sequencer_data_source<D: TestableSequencerDataSource>(
3030        #[case] _d: PhantomData<D>,
3031    ) {
3032    }
3033
3034    #[rstest_reuse::apply(testable_sequencer_data_source)]
3035    pub(crate) async fn submit_test_with_query_module<D: TestableSequencerDataSource>(
3036        _d: PhantomData<D>,
3037    ) {
3038        let storage = D::create_storage().await;
3039        submit_test_helper(|opt| D::options(&storage, opt)).await
3040    }
3041
3042    #[rstest_reuse::apply(testable_sequencer_data_source)]
3043    pub(crate) async fn status_test_with_query_module<D: TestableSequencerDataSource>(
3044        _d: PhantomData<D>,
3045    ) {
3046        let storage = D::create_storage().await;
3047        status_test_helper(|opt| D::options(&storage, opt)).await
3048    }
3049
3050    #[rstest_reuse::apply(testable_sequencer_data_source)]
3051    pub(crate) async fn state_signature_test_with_query_module<D: TestableSequencerDataSource>(
3052        _d: PhantomData<D>,
3053    ) {
3054        let storage = D::create_storage().await;
3055        state_signature_test_helper(|opt| D::options(&storage, opt)).await
3056    }
3057
3058    #[rstest_reuse::apply(testable_sequencer_data_source)]
3059    pub(crate) async fn test_namespace_query<D: TestableSequencerDataSource>(_d: PhantomData<D>) {
3060        // Arbitrary transaction, arbitrary namespace ID
3061        let ns_id = NamespaceId::from(42_u32);
3062        let txn = Transaction::new(ns_id, vec![1, 2, 3, 4]);
3063
3064        // Start query service.
3065        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
3066        let storage = D::create_storage().await;
3067        let network_config = TestConfigBuilder::default().build();
3068        let config = TestNetworkConfigBuilder::default()
3069            .api_config(D::options(&storage, Options::with_port(port)).submit(Default::default()))
3070            .network_config(network_config)
3071            .build();
3072        let network = TestNetwork::new(config, TEST_UPGRADE).await;
3073        let mut events = network.server.event_stream();
3074
3075        // Connect client.
3076        let client: Client<ClientErr, StaticVersion<0, 1>> =
3077            Client::new(format!("http://localhost:{port}").parse().unwrap());
3078        client.connect(None).await;
3079
3080        let hash = client
3081            .post("submit/submit")
3082            .body_json(&txn)
3083            .unwrap()
3084            .send()
3085            .await
3086            .unwrap();
3087        assert_eq!(txn.commit(), hash);
3088
3089        // Wait for a Decide event containing transaction matching the one we sent
3090        let block_height = wait_for_decide_on_handle(&mut events, &txn).await.0 as usize;
3091        tracing::info!(block_height, "transaction sequenced");
3092
3093        // Submit a second transaction for range queries.
3094        let txn2 = Transaction::new(ns_id, vec![5, 6, 7, 8]);
3095        client
3096            .post::<Commitment<Transaction>>("submit/submit")
3097            .body_json(&txn2)
3098            .unwrap()
3099            .send()
3100            .await
3101            .unwrap();
3102        let block_height2 = wait_for_decide_on_handle(&mut events, &txn2).await.0 as usize;
3103        tracing::info!(block_height2, "transaction sequenced");
3104
3105        // Wait for the query service to update to this block height.
3106        client
3107            .socket(&format!("availability/stream/blocks/{block_height2}"))
3108            .subscribe::<BlockQueryData<SeqTypes>>()
3109            .await
3110            .unwrap()
3111            .next()
3112            .await
3113            .unwrap()
3114            .unwrap();
3115
3116        let mut found_txn = false;
3117        let mut found_empty_block = false;
3118        for block_num in 0..=block_height {
3119            let header: Header = client
3120                .get(&format!("availability/header/{block_num}"))
3121                .send()
3122                .await
3123                .unwrap();
3124            let ns_query_res: NamespaceProofQueryData = client
3125                .get(&format!("availability/block/{block_num}/namespace/{ns_id}"))
3126                .send()
3127                .await
3128                .unwrap();
3129
3130            // Check other means of querying the same proof.
3131            assert_eq!(
3132                ns_query_res,
3133                client
3134                    .get(&format!(
3135                        "availability/block/hash/{}/namespace/{ns_id}",
3136                        header.commit()
3137                    ))
3138                    .send()
3139                    .await
3140                    .unwrap()
3141            );
3142            assert_eq!(
3143                ns_query_res,
3144                client
3145                    .get(&format!(
3146                        "availability/block/payload-hash/{}/namespace/{ns_id}",
3147                        header.payload_commitment()
3148                    ))
3149                    .send()
3150                    .await
3151                    .unwrap()
3152            );
3153
3154            // Verify namespace proof if present
3155            if let Some(ns_proof) = ns_query_res.proof {
3156                let vid_common: VidCommonQueryData<SeqTypes> = client
3157                    .get(&format!("availability/vid/common/{block_num}"))
3158                    .send()
3159                    .await
3160                    .unwrap();
3161                ns_proof
3162                    .verify(
3163                        header.ns_table(),
3164                        &header.payload_commitment(),
3165                        vid_common.common(),
3166                    )
3167                    .unwrap();
3168            } else {
3169                // Namespace proof should be present if ns_id exists in ns_table
3170                assert!(header.ns_table().find_ns_id(&ns_id).is_none());
3171                assert!(ns_query_res.transactions.is_empty());
3172            }
3173
3174            found_empty_block = found_empty_block || ns_query_res.transactions.is_empty();
3175
3176            for txn in ns_query_res.transactions {
3177                if txn.commit() == hash {
3178                    // Ensure that we validate an inclusion proof
3179                    found_txn = true;
3180                }
3181            }
3182        }
3183        assert!(found_txn);
3184        assert!(found_empty_block);
3185
3186        // Test range query.
3187        let ns_proofs: Vec<NamespaceProofQueryData> = client
3188            .get(&format!(
3189                "availability/block/{block_height}/{}/namespace/{ns_id}",
3190                block_height2 + 1
3191            ))
3192            .send()
3193            .await
3194            .unwrap();
3195        assert_eq!(ns_proofs.len(), block_height2 + 1 - block_height);
3196        assert_eq!(&ns_proofs[0].transactions, std::slice::from_ref(&txn));
3197        assert_eq!(
3198            &ns_proofs[ns_proofs.len() - 1].transactions,
3199            std::slice::from_ref(&txn2)
3200        );
3201        for proof in &ns_proofs[1..ns_proofs.len() - 1] {
3202            assert_eq!(proof.transactions, &[]);
3203        }
3204    }
3205
3206    #[rstest_reuse::apply(testable_sequencer_data_source)]
3207    pub(crate) async fn catchup_test_with_query_module<D: TestableSequencerDataSource>(
3208        _d: PhantomData<D>,
3209    ) {
3210        let storage = D::create_storage().await;
3211        catchup_test_helper(|opt| D::options(&storage, opt)).await
3212    }
3213
3214    #[rstest_reuse::apply(testable_sequencer_data_source)]
3215    pub async fn test_non_consecutive_decide_with_failing_event_consumer<D>(_d: PhantomData<D>)
3216    where
3217        D: TestableSequencerDataSource + Debug + 'static,
3218    {
3219        use hotshot_types::new_protocol::CoordinatorEvent;
3220
3221        #[derive(Clone, Copy, Debug)]
3222        struct FailConsumer;
3223
3224        #[async_trait]
3225        impl EventConsumer for FailConsumer {
3226            async fn handle_event(&self, _: &CoordinatorEvent<SeqTypes>) -> anyhow::Result<()> {
3227                bail!("mock error injection");
3228            }
3229        }
3230
3231        let (pubkey, privkey) = PubKey::generated_from_seed_indexed([0; 32], 1);
3232
3233        let storage = D::create_storage().await;
3234        let persistence = D::persistence_options(&storage).create().await.unwrap();
3235        let data_source: Arc<StorageState<_>> = Arc::new(StorageState::new(
3236            D::create(D::persistence_options(&storage), Default::default(), false)
3237                .await
3238                .unwrap(),
3239            ApiState::new(future::pending()),
3240        ));
3241
3242        // Create two non-consecutive leaf chains.
3243        let mut chain1 = vec![];
3244
3245        let node_state = NodeState::mock().with_genesis_version(NEW_PROTOCOL_VERSION);
3246        let genesis = Leaf2::genesis(&Default::default(), &node_state, TEST_UPGRADE.base).await;
3247        let payload = genesis.block_payload().unwrap();
3248        let payload_bytes_arc = payload.encode();
3249
3250        let avidm_param = init_avidm_gf2_param(2).unwrap();
3251        let weights = vec![1u32; 2];
3252
3253        let ns_table = parse_ns_table(payload.byte_len().as_usize(), &payload.ns_table().encode());
3254        let (payload_commitment, common, shares) =
3255            AvidmGf2Scheme::ns_disperse(&avidm_param, &weights, &payload_bytes_arc, ns_table)
3256                .unwrap();
3257
3258        let mut quorum_proposal = QuorumProposalWrapper::<SeqTypes> {
3259            proposal: QuorumProposal2::<SeqTypes> {
3260                block_header: genesis.block_header().clone(),
3261                view_number: ViewNumber::genesis(),
3262                justify_qc: QuorumCertificate2::genesis(
3263                    &ValidatedState::default(),
3264                    &node_state,
3265                    TEST_UPGRADE,
3266                )
3267                .await,
3268                upgrade_certificate: None,
3269                view_change_evidence: None,
3270                next_drb_result: None,
3271                next_epoch_justify_qc: None,
3272                epoch: None,
3273                state_cert: None,
3274            },
3275        };
3276        let mut qc =
3277            QuorumCertificate2::genesis(&ValidatedState::default(), &node_state, TEST_UPGRADE)
3278                .await;
3279
3280        // Long enough that the gap at view 2 falls outside `DECIDE_GAP_FILL_HORIZON` of the newest
3281        // decide, so the leaves after it are delivered rather than held for a gap-fill decide.
3282        let chain_len = 3 + DECIDE_GAP_FILL_HORIZON;
3283        let mut justify_qc = qc.clone();
3284        for i in 0..chain_len {
3285            *quorum_proposal.proposal.block_header.height_mut() = i;
3286            quorum_proposal.proposal.view_number = ViewNumber::new(i);
3287            quorum_proposal.proposal.justify_qc = justify_qc;
3288            let leaf = Leaf2::from_quorum_proposal(&quorum_proposal);
3289            qc.view_number = leaf.view_number();
3290            qc.data.leaf_commit = Committable::commit(&leaf);
3291            justify_qc = qc.clone();
3292            chain1.push((leaf.clone(), CertificatePair::non_epoch_change(qc.clone())));
3293
3294            // Include a quorum proposal for each leaf.
3295            let quorum_proposal_signature =
3296                PubKey::sign(&privkey, &bincode::serialize(&quorum_proposal).unwrap())
3297                    .expect("Failed to sign quorum_proposal");
3298            persistence
3299                .append_quorum_proposal2(&Proposal {
3300                    data: quorum_proposal.clone(),
3301                    signature: quorum_proposal_signature,
3302                    _pd: Default::default(),
3303                })
3304                .await
3305                .unwrap();
3306
3307            // Include VID information for each leaf.
3308            let share: VidDisperseShare<SeqTypes> = AvidmGf2DisperseShare {
3309                view_number: leaf.view_number(),
3310                payload_commitment,
3311                share: shares[0].clone(),
3312                recipient_key: pubkey,
3313                epoch: Some(EpochNumber::new(0)),
3314                target_epoch: Some(EpochNumber::new(0)),
3315                common: common.clone(),
3316            }
3317            .into();
3318
3319            persistence
3320                .append_vid(&share.to_proposal(&privkey).unwrap())
3321                .await
3322                .unwrap();
3323
3324            // Include payload information for each leaf.
3325            let block_payload_signature =
3326                PubKey::sign(&privkey, &payload_bytes_arc).expect("Failed to sign block payload");
3327            let da_proposal_inner = DaProposal2::<SeqTypes> {
3328                encoded_transactions: payload_bytes_arc.clone(),
3329                metadata: payload.ns_table().clone(),
3330                view_number: leaf.view_number(),
3331                epoch: Some(EpochNumber::new(0)),
3332                epoch_transition_indicator: EpochTransitionIndicator::NotInTransition,
3333            };
3334            let da_proposal = Proposal {
3335                data: da_proposal_inner,
3336                signature: block_payload_signature,
3337                _pd: Default::default(),
3338            };
3339            persistence
3340                .append_da2(&da_proposal, VidCommitment::V2(payload_commitment))
3341                .await
3342                .unwrap();
3343        }
3344        // Split into two chains.
3345        let mut chain2 = chain1.split_off(2);
3346        // Make non-consecutive (i.e. we skip a leaf).
3347        chain2.remove(0);
3348
3349        // Decide 2 leaves, but fail in event processing.
3350        let leaf_chain = chain1
3351            .iter()
3352            .map(|(leaf, qc)| (leaf_info(leaf.clone()), qc.clone()))
3353            .collect::<Vec<_>>();
3354        tracing::info!("decide with event handling failure");
3355        persistence
3356            .append_decided_leaves(
3357                ViewNumber::new(1),
3358                leaf_chain.iter().map(|(leaf, qc)| (leaf, qc.clone())),
3359                &FailConsumer,
3360            )
3361            .await
3362            .unwrap();
3363
3364        // Now decide remaining leaves successfully. We should now process a decide event for all
3365        // the leaves.
3366        let consumer = ApiEventConsumer::from(data_source.clone());
3367        let leaf_chain = chain2
3368            .iter()
3369            .map(|(leaf, qc)| (leaf_info(leaf.clone()), qc.clone()))
3370            .collect::<Vec<_>>();
3371        tracing::info!("decide successfully");
3372        persistence
3373            .append_decided_leaves(
3374                ViewNumber::new(chain_len - 1),
3375                leaf_chain.iter().map(|(leaf, qc)| (leaf, qc.clone())),
3376                &consumer,
3377            )
3378            .await
3379            .unwrap();
3380
3381        // Check that the leaves were moved to archive storage, along with payload and VID
3382        // information.
3383        for (leaf, cert) in chain1.iter().chain(&chain2) {
3384            tracing::info!(height = leaf.height(), "check archive");
3385            let qd = data_source.get_leaf(leaf.height() as usize).await.await;
3386            let stored_leaf: Leaf2 = qd.leaf().clone();
3387            let stored_qc = qd.qc().clone();
3388            assert_eq!(&stored_leaf, leaf);
3389            assert_eq!(&stored_qc, cert.qc());
3390
3391            data_source
3392                .get_block(leaf.height() as usize)
3393                .await
3394                .try_resolve()
3395                .ok()
3396                .unwrap();
3397            data_source
3398                .get_vid_common(leaf.height() as usize)
3399                .await
3400                .try_resolve()
3401                .ok()
3402                .unwrap();
3403
3404            // Check that all data has been garbage collected for the decided views.
3405            assert!(
3406                persistence
3407                    .load_da_proposal(leaf.view_number())
3408                    .await
3409                    .unwrap()
3410                    .is_none()
3411            );
3412            assert!(
3413                persistence
3414                    .load_vid_share(leaf.view_number())
3415                    .await
3416                    .unwrap()
3417                    .is_none()
3418            );
3419            assert!(
3420                persistence
3421                    .load_quorum_proposal(leaf.view_number())
3422                    .await
3423                    .is_err()
3424            );
3425        }
3426
3427        // Check that data has _not_ been garbage collected for the missing view.
3428        assert!(
3429            persistence
3430                .load_da_proposal(ViewNumber::new(2))
3431                .await
3432                .unwrap()
3433                .is_some()
3434        );
3435        assert!(
3436            persistence
3437                .load_vid_share(ViewNumber::new(2))
3438                .await
3439                .unwrap()
3440                .is_some()
3441        );
3442        persistence
3443            .load_quorum_proposal(ViewNumber::new(2))
3444            .await
3445            .unwrap();
3446    }
3447
3448    #[rstest_reuse::apply(testable_sequencer_data_source)]
3449    pub async fn test_decide_missing_data<D>(_d: PhantomData<D>)
3450    where
3451        D: TestableSequencerDataSource + Debug + 'static,
3452    {
3453        use ark_serialize::CanonicalDeserialize;
3454
3455        let storage = D::create_storage().await;
3456        let persistence = D::persistence_options(&storage).create().await.unwrap();
3457        let data_source: Arc<StorageState<_>> = Arc::new(StorageState::new(
3458            D::create(D::persistence_options(&storage), Default::default(), false)
3459                .await
3460                .unwrap(),
3461            ApiState::new(future::pending()),
3462        ));
3463        let consumer = ApiEventConsumer::from(data_source.clone());
3464
3465        let node_state = NodeState::mock().with_genesis_version(NEW_PROTOCOL_VERSION);
3466        let mut qc =
3467            QuorumCertificate2::genesis(&ValidatedState::default(), &node_state, TEST_UPGRADE)
3468                .await;
3469        let leaf = Leaf2::genesis(&ValidatedState::default(), &node_state, TEST_UPGRADE.base).await;
3470
3471        // Append the genesis leaf. We don't use this for the test, because the update function will
3472        // automatically fill in the missing data for genesis. We just append this to get into a
3473        // consistent state to then append the leaf from view 1, which will have missing data.
3474        tracing::info!(?leaf, ?qc, "decide genesis leaf");
3475        persistence
3476            .append_decided_leaves(
3477                leaf.view_number(),
3478                [(
3479                    &leaf_info(leaf.clone()),
3480                    CertificatePair::non_epoch_change(qc.clone()),
3481                )],
3482                &consumer,
3483            )
3484            .await
3485            .unwrap();
3486
3487        // Create another leaf, with missing data. We have to use a different payload commitment,
3488        // otherwise the database will be able to combine the empty payload from the genesis block
3489        // with this header, and the payload will not actually be missing.
3490        let mut block_header = leaf.block_header().clone();
3491        *block_header.height_mut() += 1;
3492        *block_header.payload_commitment_mut() = VidCommitment::V2(
3493            CanonicalDeserialize::deserialize_uncompressed_unchecked([1u8; 32].as_slice()).unwrap(),
3494        );
3495        let qp = QuorumProposalWrapper {
3496            proposal: QuorumProposal2 {
3497                block_header,
3498                view_number: leaf.view_number() + 1,
3499                justify_qc: qc.clone(),
3500                upgrade_certificate: None,
3501                view_change_evidence: None,
3502                next_drb_result: None,
3503                next_epoch_justify_qc: None,
3504                epoch: None,
3505                state_cert: None,
3506            },
3507        };
3508
3509        let leaf = Leaf2::from_quorum_proposal(&qp);
3510        qc.view_number = leaf.view_number();
3511        qc.data.leaf_commit = Committable::commit(&leaf);
3512
3513        // Decide a leaf without the corresponding payload or VID.
3514        tracing::info!(?leaf, ?qc, "append leaf 1");
3515        persistence
3516            .append_decided_leaves(
3517                leaf.view_number(),
3518                [(
3519                    &leaf_info(leaf.clone()),
3520                    CertificatePair::non_epoch_change(qc),
3521                )],
3522                &consumer,
3523            )
3524            .await
3525            .unwrap();
3526
3527        // Check that we still processed the leaf.
3528        assert_eq!(leaf, data_source.get_leaf(1).await.await.leaf().clone());
3529        assert!(data_source.get_vid_common(1).await.is_pending());
3530        assert!(data_source.get_block(1).await.is_pending());
3531    }
3532
3533    fn leaf_info(leaf: Leaf2) -> LeafInfo<SeqTypes> {
3534        LeafInfo {
3535            leaf,
3536            vid_share: None,
3537            state: Default::default(),
3538            delta: None,
3539            state_cert: None,
3540        }
3541    }
3542}
3543
3544#[cfg(test)]
3545mod test {
3546    use std::{
3547        collections::{BTreeSet, HashMap, HashSet},
3548        marker::PhantomData,
3549        time::{Duration, Instant},
3550    };
3551
3552    use ::light_client::{
3553        consensus::{
3554            header::HeaderProof,
3555            leaf::{LeafProof, LeafProofHint},
3556            namespace::NamespaceProof,
3557            payload::PayloadProof,
3558        },
3559        testing::EpochChangeQuorum,
3560    };
3561    use alloy::{
3562        eips::BlockId,
3563        network::EthereumWallet,
3564        primitives::{Address, U256},
3565        providers::{ProviderBuilder, ext::AnvilApi},
3566    };
3567    use async_lock::Mutex;
3568    use committable::{Commitment, Committable};
3569    use espresso_contract_deployer::{
3570        Contract, Contracts, builder::DeployerArgsBuilder,
3571        network_config::light_client_genesis_from_stake_table,
3572    };
3573    use espresso_types::{
3574        FeeAmount, Header, L1ClientOptions, NamespaceId, NamespaceProofQueryData, NsProof,
3575        RegisteredValidatorMap, RewardDistributor, StakeTableState, StateCertQueryDataV1,
3576        StateCertQueryDataV2, TEST_UPGRADE, ValidatedState, ValidatorLeaderCounts,
3577        config::PublicHotShotConfig,
3578        traits::{NullEventConsumer, PersistenceOptions},
3579        v0_3::{AuthenticatedValidator, COMMISSION_BASIS_POINTS, Fetcher, RewardAmount},
3580        v0_4::{RewardAccountV2, RewardMerkleProofV2},
3581        validators_from_l1_events,
3582    };
3583    use futures::{
3584        future::{self, join_all, try_join_all},
3585        stream::{StreamExt, TryStreamExt},
3586        try_join,
3587    };
3588    use hotshot_contract_adapter::{
3589        reward::{RewardAuthData, RewardClaimInput, RewardProofSiblings},
3590        sol_types::{EspToken, StakeTableV3},
3591        stake_table::StakeTableContractVersion,
3592    };
3593    use hotshot_query_service::{
3594        availability::{
3595            BlockQueryData, BlockSummaryQueryData, LeafQueryData, TransactionQueryData,
3596            VidCommonQueryData,
3597        },
3598        data_source::{
3599            Transaction as _, VersionedDataSource,
3600            storage::{StorageConnectionType, UpdateAvailabilityStorage},
3601        },
3602        explorer::TransactionSummariesResponse,
3603        node::{SyncStatus, SyncStatusQueryData},
3604        types::HeightIndexed,
3605    };
3606    use hotshot_types::{
3607        ValidatorConfig,
3608        addr::NetAddr,
3609        data::EpochNumber,
3610        epoch_membership::EpochMembershipCoordinator,
3611        event::LeafInfo,
3612        new_protocol::CoordinatorEvent,
3613        simple_certificate::UpgradeCertificate,
3614        simple_vote::UpgradeProposalData,
3615        traits::{block_contents::BlockHeader, election::Membership, metrics::NoMetrics},
3616        utils::epoch_from_block_number,
3617        x25519,
3618    };
3619    use http_client::{
3620        Client, StatusCode,
3621        error::ClientErr,
3622        healthcheck::{AppHealth, HealthStatus},
3623    };
3624    use jf_merkle_tree_compat::{
3625        MerkleTreeScheme,
3626        prelude::{MerkleProof, Sha3Node},
3627    };
3628    use pretty_assertions::assert_matches;
3629    use rand::seq::SliceRandom;
3630    use rstest::rstest;
3631    use rstest_reuse::apply;
3632    use staking_cli::{
3633        Transaction as StakingTransaction, demo::DelegationConfig, fetch_commission,
3634        update_commission, update_network_config,
3635    };
3636    use test_helpers::{
3637        TestNetwork, TestNetworkConfigBuilder, catchup_test_helper, node_storage,
3638        state_signature_test_helper, status_test_helper, submit_test_helper, wait_for_committee,
3639    };
3640    use test_utils::reserve_tcp_port;
3641    use tokio::{
3642        net::{TcpListener, TcpStream},
3643        time::sleep,
3644    };
3645    use vbs::version::StaticVersion;
3646    use versions::{
3647        DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION, EPOCH_VERSION, LARGE_BLOCK_VERSION,
3648        NEW_PROTOCOL_VERSION, Upgrade,
3649    };
3650
3651    use self::data_source::{
3652        DataSourceOptions,
3653        testing::{DeferredMigration, TestableSequencerDataSource, TestableSqlDataSource},
3654    };
3655    use super::*;
3656
3657    type Postgres = sql::postgres::DataSource;
3658    type Sqlite = sql::sqlite::DataSource;
3659
3660    #[rstest_reuse::template]
3661    #[rstest::rstest]
3662    #[case::postgres(std::marker::PhantomData::<crate::api::sql::postgres::DataSource>)]
3663    #[case::sqlite(std::marker::PhantomData::<crate::api::sql::sqlite::DataSource>)]
3664    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3665    pub(crate) fn sql_data_sources<D: TestableSqlDataSource>(
3666        #[case] _d: std::marker::PhantomData<D>,
3667    ) {
3668    }
3669
3670    async fn wait_until_block_height(
3671        client: &Client<ClientErr, StaticVersion<0, 1>>,
3672        endpoint: &str,
3673        height: u64,
3674    ) {
3675        for _retry in 0.. {
3676            let bh = client
3677                .get::<u64>(endpoint)
3678                .send()
3679                .await
3680                .expect("block height not found");
3681
3682            if bh >= height {
3683                return;
3684            }
3685            sleep(Duration::from_secs(3)).await;
3686        }
3687    }
3688    use espresso_types::{Certificate2, GenesisHeader};
3689
3690    use crate::{
3691        CatchupParams, L1Params,
3692        api::{options::Query, test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST},
3693        catchup::{NullStateCatchup, StatePeers},
3694        follower::{FollowerContext, FollowerOptions, FollowerParams, init_follower_node},
3695        genesis::{Genesis as NodeGenesis, L1Finalized, StakeTableConfig},
3696        legacy_chain::LegacyChain,
3697        persistence,
3698        persistence::no_storage,
3699        state::EmptyStateStart,
3700        testing::{
3701            TestConfig, TestConfigBuilder, decided_leaves, wait_for_decide_on_handle,
3702            wait_for_epochs,
3703        },
3704    };
3705
3706    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3707    async fn test_healthcheck() {
3708        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
3709        let url = format!("http://localhost:{port}").parse().unwrap();
3710        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
3711        let options = Options::with_port(port);
3712        let network_config = TestConfigBuilder::default().build();
3713        let config = TestNetworkConfigBuilder::<5, _, NullStateCatchup>::default()
3714            .api_config(options)
3715            .network_config(network_config)
3716            .build();
3717        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
3718
3719        client.connect(None).await;
3720        let health = client.get::<AppHealth>("healthcheck").send().await.unwrap();
3721        assert_eq!(health.status, HealthStatus::Available);
3722    }
3723
3724    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3725    async fn status_test_without_query_module() {
3726        status_test_helper(|opt| opt).await
3727    }
3728
3729    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3730    async fn submit_test_without_query_module() {
3731        submit_test_helper(|opt| opt).await
3732    }
3733
3734    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3735    async fn state_signature_test_without_query_module() {
3736        state_signature_test_helper(|opt| opt).await
3737    }
3738
3739    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3740    async fn catchup_test_without_query_module() {
3741        catchup_test_helper(|opt| opt).await
3742    }
3743
3744    #[apply(sql_data_sources)]
3745    async fn test_leaf_only_data_source<D: TestableSqlDataSource>(_d: PhantomData<D>) {
3746        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
3747
3748        let storage = D::create_storage().await;
3749        let options = D::leaf_only_ds_options(&storage, Options::with_port(port)).unwrap();
3750
3751        let network_config = TestConfigBuilder::default().build();
3752        let config = TestNetworkConfigBuilder::default()
3753            .api_config(options)
3754            .network_config(network_config)
3755            .build();
3756        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
3757        let url = format!("http://localhost:{port}").parse().unwrap();
3758        let client: Client<ClientErr, SequencerApiVersion> = Client::new(url);
3759
3760        tracing::info!("waiting for blocks");
3761        client.connect(Some(Duration::from_secs(15))).await;
3762        // Wait until some blocks have been decided.
3763
3764        let account = TestConfig::<5>::builder_key().fee_account();
3765
3766        let _headers = client
3767            .socket("availability/stream/headers/0")
3768            .subscribe::<Header>()
3769            .await
3770            .unwrap()
3771            .take(10)
3772            .try_collect::<Vec<_>>()
3773            .await
3774            .unwrap();
3775
3776        for i in 1..5 {
3777            let leaf = client
3778                .get::<LeafQueryData<SeqTypes>>(&format!("availability/leaf/{i}"))
3779                .send()
3780                .await
3781                .unwrap();
3782
3783            assert_eq!(leaf.height(), i);
3784
3785            let header = client
3786                .get::<Header>(&format!("availability/header/{i}"))
3787                .send()
3788                .await
3789                .unwrap();
3790
3791            assert_eq!(header.height(), i);
3792
3793            let vid = client
3794                .get::<VidCommonQueryData<SeqTypes>>(&format!("availability/vid/common/{i}"))
3795                .send()
3796                .await
3797                .unwrap();
3798
3799            assert_eq!(vid.height(), i);
3800
3801            client
3802                .get::<MerkleProof<Commitment<Header>, u64, Sha3Node, 3>>(&format!(
3803                    "block-state/{i}/{}",
3804                    i - 1
3805                ))
3806                .send()
3807                .await
3808                .unwrap();
3809
3810            client
3811                .get::<MerkleProof<FeeAmount, FeeAccount, Sha3Node, 256>>(&format!(
3812                    "fee-state/{}/{}",
3813                    i + 1,
3814                    account
3815                ))
3816                .send()
3817                .await
3818                .unwrap();
3819        }
3820
3821        // This would fail even though we have processed atleast 10 leaves
3822        // this is because light weight nodes only support leaves, headers and VID
3823        client
3824            .get::<BlockQueryData<SeqTypes>>("availability/block/1")
3825            .send()
3826            .await
3827            .unwrap_err();
3828    }
3829
3830    #[apply(sql_data_sources)]
3831    async fn test_database_metadata_endpoints<D: TestableSqlDataSource>(_d: PhantomData<D>) {
3832        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
3833
3834        let storage = D::create_storage().await;
3835        let options = D::options(&storage, Options::with_port(port));
3836
3837        let network_config = TestConfigBuilder::default().build();
3838        let config = TestNetworkConfigBuilder::default()
3839            .api_config(options)
3840            .network_config(network_config)
3841            .build();
3842        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
3843        let url = format!("http://localhost:{port}").parse().unwrap();
3844        let client: Client<ClientErr, SequencerApiVersion> = Client::new(url);
3845        client.connect(Some(Duration::from_secs(15))).await;
3846
3847        let table_sizes = client
3848            .get::<Vec<TableSize>>("database/table-sizes")
3849            .send()
3850            .await
3851            .unwrap();
3852        assert!(!table_sizes.is_empty());
3853
3854        // Deferred backfill migrations register tracking rows at node startup, so the list may
3855        // be non-empty; just check the entries are well-formed.
3856        let migration_status = client
3857            .get::<Vec<MigrationStatus>>("database/migration-status")
3858            .send()
3859            .await
3860            .unwrap();
3861        assert!(migration_status.iter().all(|m| !m.name.is_empty()));
3862    }
3863
3864    /// A namespace proof range ahead of the chain tip gives up like the other range endpoints,
3865    /// instead of holding one of the server's request slots until the chain gets there.
3866    #[apply(sql_data_sources)]
3867    async fn test_namespace_proof_range_ahead_of_tip_is_not_found<D: TestableSqlDataSource>(
3868        _d: PhantomData<D>,
3869    ) {
3870        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
3871        let storage = D::create_storage().await;
3872        let options = D::options(&storage, Options::with_port(port));
3873        let config = TestNetworkConfigBuilder::default()
3874            .api_config(options)
3875            .network_config(TestConfigBuilder::default().build())
3876            .build();
3877        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
3878        let url = format!("http://localhost:{port}").parse().unwrap();
3879        let client: Client<ClientErr, SequencerApiVersion> = Client::new(url);
3880        client.connect(Some(Duration::from_secs(15))).await;
3881
3882        for route in [
3883            "availability/block/1000000/1000005/namespace/0",
3884            "v2/availability/namespace-proof-range?from=1000000&until=1000005&namespace=0",
3885        ] {
3886            let err = tokio::time::timeout(
3887                Duration::from_secs(10),
3888                client.get::<serde_json::Value>(route).send(),
3889            )
3890            .await
3891            .unwrap_or_else(|_| panic!("{route} did not answer"))
3892            .expect_err(route);
3893            assert_eq!(err.status, StatusCode::NOT_FOUND, "{route}");
3894        }
3895    }
3896
3897    /// A pruned node keeps no totals from genesis, so every route that serves them is a 404 on v1
3898    /// and v2, even for a range of blocks the node stores.
3899    #[apply(sql_data_sources)]
3900    async fn test_pruned_node_totals_are_not_found<D: TestableSqlDataSource>(_d: PhantomData<D>) {
3901        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
3902        let storage = D::create_storage().await;
3903        let mut db_opt = D::tmp_options(&storage);
3904        db_opt.prune = true;
3905        let config = TestNetworkConfigBuilder::default()
3906            .api_config(Options::with_port(port).query_sql(Query::default(), db_opt))
3907            .network_config(TestConfigBuilder::default().build())
3908            .build();
3909        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
3910        let client: Client<ClientErr, StaticVersion<0, 1>> =
3911            Client::new(format!("http://localhost:{port}").parse().unwrap());
3912        assert!(client.connect(Some(Duration::from_secs(60))).await);
3913        tokio::time::timeout(
3914            Duration::from_secs(120),
3915            wait_until_block_height(&client, "status/block-height", 2),
3916        )
3917        .await
3918        .expect("network did not reach block height 2");
3919
3920        for route in [
3921            "node/transactions/count",
3922            "node/transactions/count/0/1",
3923            "node/transactions/count/namespace/0",
3924            "node/payloads/size",
3925            "node/payloads/size/0/1",
3926            "node/payloads/size/namespace/0",
3927            "explorer/explorer-summary",
3928            "v2/node/transaction-count",
3929            "v2/node/transaction-count?from=0&to=1",
3930            "v2/node/transaction-count?namespace=0",
3931            "v2/node/payload-size",
3932            "v2/node/payload-size?from=0&to=1",
3933            "v2/node/payload-size?namespace=0",
3934            "v2/explorer/summary",
3935        ] {
3936            let err = client
3937                .get::<serde_json::Value>(route)
3938                .send()
3939                .await
3940                .expect_err(route);
3941            assert_eq!(err.status, StatusCode::NOT_FOUND, "{route}");
3942        }
3943    }
3944
3945    /// Typed light client errors reach the HTTP client with their own status, not as a 500.
3946    #[apply(sql_data_sources)]
3947    async fn test_light_client_proof_errors_keep_their_status<D: TestableSqlDataSource>(
3948        _d: PhantomData<D>,
3949    ) {
3950        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
3951        let storage = D::create_storage().await;
3952        let config = TestNetworkConfigBuilder::default()
3953            .api_config(D::options(&storage, Options::with_port(port)))
3954            .network_config(TestConfigBuilder::default().build())
3955            .build();
3956        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
3957
3958        let client: Client<ClientErr, StaticVersion<0, 1>> =
3959            Client::new(format!("http://localhost:{port}").parse().unwrap());
3960        assert!(client.connect(Some(Duration::from_secs(60))).await);
3961        tokio::time::timeout(
3962            Duration::from_secs(120),
3963            wait_until_block_height(&client, "status/block-height", 2),
3964        )
3965        .await
3966        .expect("network did not reach block height 2");
3967
3968        let past_limit =
3969            hotshot_query_service::availability::Options::default().large_object_range_limit + 1;
3970        for (path, status) in [
3971            // A finalized height must be past the requested leaf.
3972            ("leaf/1/1".to_string(), reqwest::StatusCode::BAD_REQUEST),
3973            // A header proof's root must be past the requested header.
3974            ("header/1/1".to_string(), reqwest::StatusCode::BAD_REQUEST),
3975            // This 404 comes from the header proof helper, exercising `lc_error`.
3976            (
3977                "header/1000001/1000000".to_string(),
3978                reqwest::StatusCode::NOT_FOUND,
3979            ),
3980            (
3981                format!("payload/0/{past_limit}"),
3982                reqwest::StatusCode::BAD_REQUEST,
3983            ),
3984        ] {
3985            let res = reqwest::get(format!("http://localhost:{port}/v1/light-client/{path}"))
3986                .await
3987                .unwrap();
3988            assert_eq!(res.status(), status, "{path}");
3989        }
3990    }
3991
3992    async fn run_catchup_test<D: TestableSqlDataSource>(url_suffix: &str) {
3993        // Start a sequencer network, using the query service for catchup. Node 0 keeps merklized
3994        // state in SQL: in memory it holds only the states from its decided view up, and a node
3995        // restarted under load handles proposals behind the network, so by the time it asks for
3996        // a parent's frontier a peer without storage has dropped that view, and the request
3997        // retries forever.
3998        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
3999        const NUM_NODES: usize = 5;
4000        let storage = D::create_storage().await;
4001
4002        let url: url::Url = format!("http://localhost:{port}{url_suffix}")
4003            .parse()
4004            .unwrap();
4005
4006        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
4007            .api_config(D::options(&storage, Options::with_port(port)))
4008            .network_config(TestConfigBuilder::default().build())
4009            .without_api_catchup()
4010            .catchups(std::array::from_fn(|_| {
4011                StatePeers::<StaticVersion<0, 1>>::from_urls(
4012                    vec![url.clone()],
4013                    Default::default(),
4014                    Duration::from_secs(2),
4015                    &NoMetrics,
4016                )
4017            }))
4018            .build();
4019        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
4020        let catchup = StatePeers::<StaticVersion<0, 1>>::from_urls(
4021            vec![url],
4022            Default::default(),
4023            Duration::from_secs(2),
4024            &NoMetrics,
4025        );
4026        restarted_node_catches_up(&mut network, catchup).await;
4027    }
4028
4029    /// Restarts replica 1 from genesis after the network has moved on without it, and waits for
4030    /// it to decide blocks past the height the network had reached when it came back, with a
4031    /// state it validated itself. We don't just stop consensus and restart it; we fully drop the
4032    /// node and recreate it so it loses all of its temporary state. It should be able to catch up
4033    /// by listening to proposals and then rebuild its state with `catchup`.
4034    async fn restarted_node_catches_up<const NUM_NODES: usize>(
4035        network: &mut TestNetwork<no_storage::Options, NUM_NODES>,
4036        catchup: impl StateCatchup + 'static,
4037    ) {
4038        // A decide proves only that the node hears the network: it takes a proposal and its two
4039        // certificates, all broadcast, and never a validated state. A state this node computed
4040        // carries a delta, where the `from_header` stub it holds otherwise has none.
4041        restart_from_genesis_and_decide(network, catchup, |info| info.delta.is_some()).await;
4042    }
4043
4044    /// Restarts replica 1 from genesis after the network has moved on without it, then waits for
4045    /// it to decide a leaf past the height the network had reached when it came back that also
4046    /// satisfies `caught_up`. Returns every leaf it decided on the way, oldest first.
4047    async fn restart_from_genesis_and_decide<const NUM_NODES: usize>(
4048        network: &mut TestNetwork<no_storage::Options, NUM_NODES>,
4049        catchup: impl StateCatchup + 'static,
4050        caught_up: impl Fn(&LeafInfo<SeqTypes>) -> bool,
4051    ) -> Vec<LeafInfo<SeqTypes>> {
4052        // Wait for replica 1 to reach a (non-genesis) decide, before disconnecting it.
4053        let mut events = network.node(1).event_stream();
4054        loop {
4055            let event = events.next().await.unwrap();
4056            let Some(leaf_chain) = decided_leaves(&event) else {
4057                continue;
4058            };
4059            if leaf_chain[0].leaf.height() > 0 {
4060                break;
4061            }
4062        }
4063
4064        tracing::info!("shutting down node");
4065        network.stop_node(1).await;
4066
4067        // Wait for a few blocks to pass while the node is down, so it falls behind.
4068        network
4069            .server
4070            .event_stream()
4071            .filter(|event| future::ready(decided_leaves(event).is_some()))
4072            .take(3)
4073            .collect::<Vec<_>>()
4074            .await;
4075
4076        tracing::info!("restarting node");
4077        let target = network.server.decided_leaf().await.height() + 3;
4078        let node = network
4079            .start_stopped_node(
4080                1,
4081                network.genesis_state(1),
4082                no_storage::Options,
4083                catchup,
4084                TEST_UPGRADE,
4085            )
4086            .await;
4087        let mut events = node.event_stream();
4088        let mut decided = Vec::new();
4089        let done = timeout(Duration::from_secs(120), async {
4090            loop {
4091                let event = events.next().await.unwrap();
4092                let Some(chain) = decided_leaves(&event) else {
4093                    continue;
4094                };
4095                decided.extend(chain.iter().rev().cloned());
4096                let newest = &chain[0];
4097                if newest.leaf.height() >= target && caught_up(newest) {
4098                    break;
4099                }
4100            }
4101        })
4102        .await;
4103        assert!(
4104            done.is_ok(),
4105            "restarted node never caught up to height {target}; decided (height, validated): {:?}",
4106            decided
4107                .iter()
4108                .map(|info| (info.leaf.height(), info.delta.is_some()))
4109                .collect::<Vec<_>>()
4110        );
4111        decided
4112    }
4113
4114    #[apply(sql_data_sources)]
4115    async fn test_catchup<D: TestableSqlDataSource>(_d: PhantomData<D>) {
4116        run_catchup_test::<D>("").await;
4117    }
4118
4119    #[apply(sql_data_sources)]
4120    async fn test_catchup_v0<D: TestableSqlDataSource>(_d: PhantomData<D>) {
4121        run_catchup_test::<D>("/v0").await;
4122    }
4123
4124    #[apply(sql_data_sources)]
4125    async fn test_catchup_v1<D: TestableSqlDataSource>(_d: PhantomData<D>) {
4126        run_catchup_test::<D>("/v1").await;
4127    }
4128
4129    /// Without HTTP state peers, a node restarted from genesis seeds a stub for its first decided
4130    /// leaf and catches up over request-response at that decided view, which peers still hold.
4131    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4132    async fn test_restart_no_state_peers_catches_up_over_request_response() {
4133        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4134        const NUM_NODES: usize = 5;
4135        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
4136            .api_config(Options::with_port(port))
4137            .network_config(TestConfigBuilder::default().build())
4138            .without_api_catchup()
4139            .build();
4140        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
4141        restart_from_genesis_and_decide(&mut network, NullStateCatchup::default(), |info| {
4142            info.delta.is_some()
4143        })
4144        .await;
4145    }
4146
4147    /// A query node with a gap must fill it from a peer over the path production runs: the
4148    /// proactive scanner, `LightClientProvider`, the light-client HTTP client and the peer's
4149    /// endpoints. One node takes part in consensus while the chain is built, then is shut down and
4150    /// brought back with a query database holding every leaf and one empty payload, the state a
4151    /// node is in once its leaf scan is done: payload dedup reads every empty height as present,
4152    /// and the non-empty ones are the fragmented set mainnet leaves behind.
4153    ///
4154    /// What this pins is that the production wiring converges on the peer's data, not that the
4155    /// ranges endpoints carried it. Consensus is not restarted, but two other writers remain: the
4156    /// startup replay of decides persisted before the shutdown, and the aggregator's payload
4157    /// fetches by hash. Both are bounded and neither uses a ranges request, so they cost the test
4158    /// its claim on the route rather than its result;
4159    /// `test_scanner_backfills_over_ranges_endpoints_only` is what covers the route.
4160    #[apply(sql_data_sources)]
4161    async fn test_query_service_catchup_from_peer<D>(_d: PhantomData<D>)
4162    where
4163        D: TestableSqlDataSource,
4164        for<'a> <D as VersionedDataSource>::Transaction<'a>: UpdateAvailabilityStorage<SeqTypes>,
4165    {
4166        const NUM_NODES: usize = 5;
4167        const LATE: usize = NUM_NODES - 1;
4168        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4169        let late_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4170        let peer_url: Url = format!("http://localhost:{port}").parse().unwrap();
4171        let state_peers = || {
4172            StatePeers::<SequencerApiVersion>::from_urls(
4173                vec![peer_url.clone()],
4174                Default::default(),
4175                Duration::from_secs(2),
4176                &NoMetrics,
4177            )
4178        };
4179
4180        let dbs = join_all((0..NUM_NODES).map(|_| D::create_storage())).await;
4181        let persistence: [_; NUM_NODES] = dbs
4182            .iter()
4183            .map(<D as TestableSequencerDataSource>::persistence_options)
4184            .collect::<Vec<_>>()
4185            .try_into()
4186            .unwrap();
4187        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
4188            .api_config(D::options(&dbs[0], Options::with_port(port)))
4189            .persistences(persistence)
4190            .catchups(std::array::from_fn(|_| state_peers()))
4191            .network_config(TestConfigBuilder::default().build())
4192            .build();
4193        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
4194
4195        // Interleave transactions with idle views so the chain gets both non-empty and empty
4196        // blocks. Which heights land either way is up to the builder, so the shape is read back
4197        // from the peer below rather than assumed here.
4198        let namespace = NamespaceId::from(7_u32);
4199        let mut decided = network.server.event_stream().filter_map(|event| {
4200            future::ready(match event {
4201                CoordinatorEvent::NewDecide { leaf_infos, .. } => Some(leaf_infos[0].leaf.clone()),
4202                _ => None,
4203            })
4204        });
4205        let height = tokio::time::timeout(Duration::from_secs(180), async {
4206            let mut height = 0;
4207            for i in 0..4u8 {
4208                network
4209                    .server
4210                    .submit_transaction(Transaction::new(namespace, vec![i; 8]))
4211                    .await
4212                    .unwrap();
4213                // Wait for it to be sequenced, then let a couple more views decide.
4214                loop {
4215                    let leaf = decided.next().await.unwrap();
4216                    height = leaf.height();
4217                    if leaf
4218                        .block_header()
4219                        .ns_table()
4220                        .find_ns_id(&namespace)
4221                        .is_some()
4222                    {
4223                        break;
4224                    }
4225                }
4226                for _ in 0..2 {
4227                    height = decided.next().await.unwrap().height();
4228                }
4229            }
4230            // A finality proof for a leaf needs a QC two-chain above it, so leave the last
4231            // non-empty block well below the tip.
4232            for _ in 0..3 {
4233                height = decided.next().await.unwrap().height();
4234            }
4235            height
4236        })
4237        .await
4238        .expect("network did not sequence the test transactions");
4239        drop(decided);
4240
4241        // Take the last node out of consensus; it comes back below as the late query node. As in
4242        // `restart_node`, its coordinator port is only free once the aborted tasks let go of it.
4243        network.peers[LATE - 1].shut_down().await;
4244        let addr = network.cfg.coordinator_addr(LATE).to_string();
4245        tokio::time::timeout(Duration::from_secs(60), async {
4246            while std::net::TcpListener::bind(&addr).is_err() {
4247                sleep(Duration::from_millis(100)).await;
4248            }
4249        })
4250        .await
4251        .expect("shut-down node did not release its coordinator port");
4252
4253        // Seed the late node with every leaf plus one empty block, the state a node reaches once
4254        // its leaf scan is done. Payload dedup then makes every empty height read as present. Not
4255        // the genesis block: its payload commitment is not the one the chain's empty blocks share.
4256        let peer: Client<ClientErr, SequencerApiVersion> = Client::new(peer_url.clone());
4257        let leaves: Vec<LeafQueryData<SeqTypes>> = peer
4258            .get(&format!("availability/leaf/0/{}", height + 1))
4259            .send()
4260            .await
4261            .unwrap();
4262        let blocks: Vec<BlockQueryData<SeqTypes>> = peer
4263            .get(&format!("availability/block/0/{}", height + 1))
4264            .send()
4265            .await
4266            .unwrap();
4267        let (empty, non_empty): (Vec<_>, Vec<_>) = blocks
4268            .iter()
4269            .filter(|block| block.height() > 0)
4270            .partition(|block| block.num_transactions() == 0);
4271        assert!(
4272            non_empty.len() >= 2 && !empty.is_empty(),
4273            "chain of {} heights is not a mix of empty and non-empty blocks",
4274            blocks.len()
4275        );
4276        {
4277            // No proactive fetching: this handle only seeds, and a scanner of its own would race
4278            // the late node for the same database.
4279            let mut opt = D::tmp_options(&dbs[LATE]);
4280            opt.disable_proactive_fetching = true;
4281            let ds = D::create(D::sql_persistence_options(opt), Default::default(), false)
4282                .await
4283                .unwrap();
4284            let mut tx = ds.write().await.unwrap();
4285            for leaf in &leaves {
4286                tx.insert_leaf(leaf).await.unwrap();
4287            }
4288            // Genesis too, so the only missing blocks are the non-empty heights: its payload
4289            // commitment is its own, so it dedups with nothing and would otherwise read as one
4290            // more missing run of its own.
4291            tx.insert_block(&blocks[0]).await.unwrap();
4292            tx.insert_block(empty[0]).await.unwrap();
4293            tx.commit().await.unwrap();
4294
4295            // Read the shape back rather than over HTTP, where the scanner could have run first.
4296            // The missing blocks are exactly the non-empty heights now, so more than one run is
4297            // the fragmentation this whole path exists for; one contiguous gap would be a much
4298            // easier case.
4299            let status = ds.sync_status().await.unwrap();
4300            let missing_runs = status
4301                .blocks
4302                .ranges
4303                .iter()
4304                .filter(|range| range.status == SyncStatus::Missing)
4305                .count();
4306            assert!(
4307                missing_runs > 1,
4308                "missing set is not fragmented: {status:#?}"
4309            );
4310            assert!(status.vid_common.missing > 0, "{status:#?}");
4311        }
4312
4313        // Back as a query node whose service fetches from node 0 the way a production node fetches
4314        // from its configured peers. Consensus is not restarted: a decided leaf would have the node
4315        // chase parents one at a time, and that is not the path under test.
4316        let mut late_db = D::tmp_options(&dbs[LATE]);
4317        late_db.proactive_scan_interval = Some(Duration::from_secs(1));
4318        // A ranges request that cannot be served costs this much before the per-chunk fallback, and the
4319        // 120 second default would not fit in the budget below.
4320        late_db.proactive_fetch_timeout = Some(Duration::from_secs(5));
4321        // The sync status the scanner reads and the endpoint serves is cached for five minutes by
4322        // default, which would hide the catch-up from both for the whole test.
4323        late_db.sync_status_ttl = Some(Duration::from_secs(1));
4324        // Chunks smaller than the gap, so the missing runs pack into several requests.
4325        late_db.proactive_scan_chunk_size = Some(4);
4326        let api = Options::with_port(late_port).query_sql(
4327            Query {
4328                peers: vec![peer_url.clone()],
4329                ..Default::default()
4330            },
4331            late_db,
4332        );
4333        let cfg = network.cfg.clone();
4334        let upgrades_map = cfg.upgrades();
4335        let persistence = <D as TestableSequencerDataSource>::persistence_options(&dbs[LATE]);
4336        let catchup = state_peers();
4337        let _late = api
4338            .serve(|metrics, consumer, storage| {
4339                async move {
4340                    Ok(cfg
4341                        .init_node(
4342                            LATE,
4343                            ValidatedState::default(),
4344                            persistence,
4345                            Some(catchup),
4346                            storage,
4347                            &*metrics,
4348                            STAKE_TABLE_CAPACITY_FOR_TEST,
4349                            consumer,
4350                            TEST_UPGRADE,
4351                            upgrades_map,
4352                        )
4353                        .await)
4354                }
4355                .boxed()
4356            })
4357            .await
4358            .unwrap();
4359
4360        let client: Client<ClientErr, SequencerApiVersion> =
4361            Client::new(format!("http://localhost:{late_port}").parse().unwrap());
4362        assert!(client.connect(Some(Duration::from_secs(60))).await);
4363        let mut last = None;
4364        let synced = tokio::time::timeout(Duration::from_secs(180), async {
4365            loop {
4366                if let Ok(status) = client
4367                    .get::<SyncStatusQueryData>("node/sync-status")
4368                    .send()
4369                    .await
4370                {
4371                    if status.is_fully_synced() {
4372                        return;
4373                    }
4374                    tracing::info!(?status, height, "waiting for the late node to catch up");
4375                    last = Some(status);
4376                }
4377                sleep(Duration::from_secs(1)).await;
4378            }
4379        })
4380        .await;
4381        assert!(
4382            synced.is_ok(),
4383            "late node did not catch its query service up from its peer; last status {last:#?}"
4384        );
4385
4386        for h in 0..=height {
4387            let (ours, theirs) = try_join!(
4388                client
4389                    .get::<BlockQueryData<SeqTypes>>(&format!("availability/block/{h}"))
4390                    .send(),
4391                peer.get::<BlockQueryData<SeqTypes>>(&format!("availability/block/{h}"))
4392                    .send(),
4393            )
4394            .unwrap();
4395            assert_eq!(ours, theirs);
4396            let (ours, theirs) = try_join!(
4397                client
4398                    .get::<VidCommonQueryData<SeqTypes>>(&format!("availability/vid/common/{h}"))
4399                    .send(),
4400                peer.get::<VidCommonQueryData<SeqTypes>>(&format!("availability/vid/common/{h}"))
4401                    .send(),
4402            )
4403            .unwrap();
4404            assert_eq!(ours, theirs);
4405        }
4406    }
4407
4408    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4409    async fn test_chain_config_catchup() {
4410        // Only node 0 starts with the full chain config; every other node holds just its
4411        // commitment and must fetch the config from node 0.
4412        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4413
4414        const NUM_NODES: usize = 5;
4415        let builder = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
4416            .api_config(Options::from(options::Http {
4417                port,
4418                max_connections: None,
4419                tonic_port: None,
4420            }))
4421            .catchups(std::array::from_fn(|_| {
4422                StatePeers::<StaticVersion<0, 1>>::from_urls(
4423                    vec![format!("http://localhost:{port}").parse().unwrap()],
4424                    Default::default(),
4425                    Duration::from_secs(2),
4426                    &NoMetrics,
4427                )
4428            }))
4429            .network_config(TestConfigBuilder::default().build())
4430            .pos_hook(
4431                DelegationConfig::MultipleDelegators,
4432                StakeTableContractVersion::V3,
4433                TEST_UPGRADE,
4434            )
4435            .await
4436            .unwrap();
4437
4438        // Differ from the default so a node can't get it from its own instance state.
4439        let cf = ChainConfig {
4440            max_block_size: 300.into(),
4441            ..builder.chain_config()
4442        };
4443        let commitment_only = ValidatedState {
4444            chain_config: cf.commit().into(),
4445            ..Default::default()
4446        };
4447        let mut states = std::array::from_fn(|_| commitment_only.clone());
4448        states[0] = ValidatedState {
4449            chain_config: cf.into(),
4450            ..Default::default()
4451        };
4452        let config = builder.states(states).genesis_chain_config(cf).build();
4453
4454        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
4455
4456        // Wait for a few blocks to be decided.
4457        network
4458            .server
4459            .event_stream()
4460            .filter(|event| future::ready(decided_leaves(event).is_some()))
4461            .take(3)
4462            .collect::<Vec<_>>()
4463            .await;
4464
4465        for node in std::iter::once(&network.server).chain(&network.peers) {
4466            let state = node.consensus_handle().decided_state().await.unwrap();
4467
4468            assert_eq!(state.chain_config.resolve().unwrap(), cf)
4469        }
4470
4471        network.server.shut_down().await;
4472        drop(network);
4473    }
4474
4475    /// A coordinator that dies is not papered over: `join` returns so the node exits, and the
4476    /// readers fail instead of serving the resting state as if the chain had stopped.
4477    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4478    async fn test_dead_coordinator_is_visible() {
4479        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4480        let config = TestNetworkConfigBuilder::default()
4481            .api_config(Options::with_port(port))
4482            .network_config(TestConfigBuilder::default().build())
4483            .build();
4484        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
4485        let node = &mut network.peers[0];
4486        while node.decided_leaf().await.height() < 1 {
4487            sleep(Duration::from_millis(100)).await;
4488        }
4489
4490        let handle = node.consensus_handle();
4491        handle.kill_coordinator();
4492
4493        timeout(Duration::from_secs(10), node.join())
4494            .await
4495            .expect("join did not return after the coordinator died");
4496        let read = tokio::spawn(async move { handle.decided_leaf().await }).await;
4497        assert!(
4498            read.is_err_and(|err| err.is_panic()),
4499            "a dead coordinator was served from the resting state"
4500        );
4501    }
4502
4503    /// After shutdown the readers serve the coordinator's last decide, not the anchor the node
4504    /// started from, and the leaf and state they serve belong to the same block.
4505    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4506    async fn test_shut_down_keeps_last_decided_leaf_and_state() {
4507        const HEIGHT: u64 = 5;
4508        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4509        let config = TestNetworkConfigBuilder::default()
4510            .api_config(Options::with_port(port))
4511            .network_config(TestConfigBuilder::default().build())
4512            .build();
4513        let network = TestNetwork::new(config, TEST_UPGRADE).await;
4514        let node = &network.peers[0];
4515        while node.decided_leaf().await.height() < HEIGHT {
4516            sleep(Duration::from_millis(100)).await;
4517        }
4518
4519        node.shutdown_consensus().await;
4520
4521        let leaf = node.decided_leaf().await;
4522        let state = node.decided_state().await.expect("state after shutdown");
4523        assert!(
4524            leaf.height() >= HEIGHT,
4525            "anchor served instead of the last decide"
4526        );
4527        // The state at height `h` holds the commitments of the `h` headers before it.
4528        assert_eq!(
4529            state.block_merkle_tree.num_leaves(),
4530            leaf.height(),
4531            "state does not belong to the resting leaf"
4532        );
4533    }
4534
4535    /// A node shut down before it ever started consensus stays stopped: a later read serves the
4536    /// anchor instead of lazily starting the coordinator after shutdown.
4537    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4538    async fn test_shut_down_before_start_keeps_consensus_stopped() {
4539        const NUM_NODES: usize = 3;
4540        const LATE: usize = NUM_NODES - 1;
4541        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4542        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
4543            .api_config(Options::with_port(port))
4544            .network_config(TestConfigBuilder::default().build())
4545            .deferred_start(&[LATE])
4546            .build();
4547        let network = TestNetwork::new(config, TEST_UPGRADE).await;
4548
4549        let late = network
4550            .cfg
4551            .init_node(
4552                LATE,
4553                network.genesis_state(LATE),
4554                no_storage::Options,
4555                Some(NullStateCatchup::default()),
4556                None,
4557                &NoMetrics,
4558                STAKE_TABLE_CAPACITY_FOR_TEST,
4559                NullEventConsumer,
4560                TEST_UPGRADE,
4561                network.cfg.upgrades(),
4562            )
4563            .await;
4564        let handle = late.consensus_handle();
4565
4566        // No read before the shutdown: a read would lazily start consensus.
4567        late.shutdown_consensus().await;
4568
4569        assert_eq!(late.decided_leaf().await.height(), 0, "not the anchor");
4570        assert!(
4571            handle.client_api().await.is_none(),
4572            "a read started consensus after shutdown"
4573        );
4574        handle.start_consensus().await;
4575        assert!(
4576            handle.client_api().await.is_none(),
4577            "consensus restarted after shutdown"
4578        );
4579    }
4580
4581    /// A genesis file whose base version predates the new protocol, as a custom genesis left at
4582    /// the previous base is.
4583    const PRE_NEW_PROTOCOL_UPGRADE: Upgrade =
4584        Upgrade::new(EPOCH_REWARD_VERSION, NEW_PROTOCOL_VERSION);
4585
4586    /// Initializes a node from `persistence` with [`PRE_NEW_PROTOCOL_UPGRADE`], without starting
4587    /// its consensus. The network is returned with the result because it owns the L1 the node
4588    /// talks to.
4589    async fn try_init_pre_new_protocol_node<P: PersistenceOptions>(
4590        persistence: P,
4591    ) -> (
4592        TestNetwork<no_storage::Options, 2>,
4593        anyhow::Result<SequencerContext<P::Persistence>>,
4594    ) {
4595        const NUM_NODES: usize = 2;
4596        const LATE: usize = NUM_NODES - 1;
4597        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4598        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
4599            .api_config(Options::with_port(port))
4600            .network_config(TestConfigBuilder::default().build())
4601            .deferred_start(&[LATE])
4602            .build();
4603        let network = TestNetwork::new(config, TEST_UPGRADE).await;
4604        let node = network
4605            .cfg
4606            .try_init_node(
4607                LATE,
4608                network.genesis_state(LATE),
4609                persistence,
4610                Some(NullStateCatchup::default()),
4611                None,
4612                &NoMetrics,
4613                STAKE_TABLE_CAPACITY_FOR_TEST,
4614                NullEventConsumer,
4615                PRE_NEW_PROTOCOL_UPGRADE,
4616                network.cfg.upgrades(),
4617            )
4618            .await;
4619        (network, node)
4620    }
4621
4622    /// With no decided upgrade, a chain at a base version before the new protocol is refused:
4623    /// this node runs no other consensus.
4624    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4625    async fn test_init_refuses_undecided_pre_new_protocol_base() {
4626        let (_network, node) = try_init_pre_new_protocol_node(no_storage::Options).await;
4627        let err = node.expect_err("a chain before the new protocol started");
4628        assert!(
4629            format!("{err:#}").contains("predates the new protocol"),
4630            "unexpected error: {err:#}"
4631        );
4632    }
4633
4634    /// The gate checks the version the chain decided, not the configured base: storage holding
4635    /// the decided upgrade to the new protocol starts even though genesis still says the old base.
4636    #[apply(sql_data_sources)]
4637    async fn test_init_starts_decided_new_protocol_from_older_base<D: TestableSqlDataSource>(
4638        _d: PhantomData<D>,
4639    ) {
4640        let storage = D::create_storage().await;
4641        let mut persistence = <D as TestableSequencerDataSource>::persistence_options(&storage);
4642        let decided = {
4643            let data = UpgradeProposalData {
4644                old_version: PRE_NEW_PROTOCOL_UPGRADE.base,
4645                new_version: PRE_NEW_PROTOCOL_UPGRADE.target,
4646                decide_by: ViewNumber::genesis(),
4647                new_version_hash: Default::default(),
4648                old_version_last_view: ViewNumber::genesis(),
4649                new_version_first_view: ViewNumber::genesis(),
4650            };
4651            UpgradeCertificate::<SeqTypes>::new(
4652                data.clone(),
4653                data.commit(),
4654                ViewNumber::genesis(),
4655                Default::default(),
4656                Default::default(),
4657            )
4658        };
4659        persistence
4660            .create()
4661            .await
4662            .unwrap()
4663            .store_upgrade_certificate(Some(decided.clone()))
4664            .await
4665            .unwrap();
4666
4667        let (_network, node) = try_init_pre_new_protocol_node(persistence).await;
4668        let node = node.expect("a decided upgrade to the new protocol starts");
4669        assert_eq!(
4670            node.upgrade_lock().decided_upgrade_cert(),
4671            Some(decided),
4672            "the restored lock lost the decided upgrade"
4673        );
4674        node.shutdown_consensus().await;
4675    }
4676
4677    #[apply(sql_data_sources)]
4678    pub(crate) async fn test_restart<D: TestableSqlDataSource>(_d: PhantomData<D>) {
4679        const NUM_NODES: usize = 5;
4680        // Initialize nodes.
4681        let storage = join_all((0..NUM_NODES).map(|_| D::create_storage())).await;
4682        let persistence: [_; NUM_NODES] = storage
4683            .iter()
4684            .map(<D as TestableSequencerDataSource>::persistence_options)
4685            .collect::<Vec<_>>()
4686            .try_into()
4687            .unwrap();
4688        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4689        let config = TestNetworkConfigBuilder::default()
4690            .api_config(D::options(&storage[0], Options::with_port(port)))
4691            .persistences(persistence.clone())
4692            .network_config(TestConfigBuilder::default().build())
4693            .build();
4694        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
4695
4696        // Connect client.
4697        let client: Client<ClientErr, SequencerApiVersion> =
4698            Client::new(format!("http://localhost:{port}").parse().unwrap());
4699        client.connect(None).await;
4700        tracing::info!(port, "server running");
4701
4702        // Wait until some blocks have been decided.
4703        client
4704            .socket("availability/stream/blocks/0")
4705            .subscribe::<BlockQueryData<SeqTypes>>()
4706            .await
4707            .unwrap()
4708            .take(3)
4709            .collect::<Vec<_>>()
4710            .await;
4711
4712        // Shut down the consensus nodes.
4713        tracing::info!("shutting down nodes");
4714        network.stop_consensus().await;
4715
4716        // Get the block height we reached.
4717        let height = client
4718            .get::<usize>("status/block-height")
4719            .send()
4720            .await
4721            .unwrap();
4722        tracing::info!("decided {height} blocks before shutting down");
4723
4724        // Get the decided chain, so we can check consistency after the restart.
4725        let chain: Vec<LeafQueryData<SeqTypes>> = client
4726            .socket("availability/stream/leaves/0")
4727            .subscribe()
4728            .await
4729            .unwrap()
4730            .take(height)
4731            .try_collect()
4732            .await
4733            .unwrap();
4734        let decided_view = chain.last().unwrap().leaf().view_number();
4735
4736        // Get the most recent state, for catchup.
4737
4738        let state = network.server.decided_state().await.unwrap();
4739        tracing::info!(?decided_view, ?state, "consensus state");
4740
4741        // Fully shut down the API servers.
4742        drop(network);
4743
4744        // Start up again, resuming from the last decided leaf.
4745        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4746
4747        let config = TestNetworkConfigBuilder::default()
4748            .api_config(D::options(&storage[0], Options::with_port(port)))
4749            .persistences(persistence)
4750            .catchups(std::array::from_fn(|_| {
4751                // Catchup using node 0 as a peer. Node 0 was running the archival state service
4752                // before the restart, so it should be able to resume without catching up by loading
4753                // state from storage.
4754                StatePeers::<StaticVersion<0, 1>>::from_urls(
4755                    vec![format!("http://localhost:{port}").parse().unwrap()],
4756                    Default::default(),
4757                    Duration::from_secs(2),
4758                    &NoMetrics,
4759                )
4760            }))
4761            .network_config(TestConfigBuilder::default().build())
4762            .build();
4763        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
4764        let client: Client<ClientErr, StaticVersion<0, 1>> =
4765            Client::new(format!("http://localhost:{port}").parse().unwrap());
4766        client.connect(None).await;
4767        tracing::info!(port, "server running");
4768
4769        // Make sure we can decide new blocks after the restart.
4770        tracing::info!("waiting for decide, height {height}");
4771        let new_leaf: LeafQueryData<SeqTypes> = client
4772            .socket(&format!("availability/stream/leaves/{height}"))
4773            .subscribe()
4774            .await
4775            .unwrap()
4776            .next()
4777            .await
4778            .unwrap()
4779            .unwrap();
4780        assert_eq!(new_leaf.height(), height as u64);
4781        assert_eq!(
4782            new_leaf.leaf().parent_commitment(),
4783            chain[height - 1].hash()
4784        );
4785
4786        // Ensure the new chain is consistent with the old chain.
4787        let new_chain: Vec<LeafQueryData<SeqTypes>> = client
4788            .socket("availability/stream/leaves/0")
4789            .subscribe()
4790            .await
4791            .unwrap()
4792            .take(height)
4793            .try_collect()
4794            .await
4795            .unwrap();
4796        assert_eq!(chain, new_chain);
4797    }
4798
4799    #[test_log::test(tokio::test(flavor = "multi_thread"))]
4800    async fn test_fetch_config() {
4801        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4802        let url: Url = format!("http://localhost:{port}").parse().unwrap();
4803        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url.clone());
4804
4805        let options = Options::with_port(port).config(Default::default());
4806        let network_config = TestConfigBuilder::default().build();
4807        let config = TestNetworkConfigBuilder::default()
4808            .api_config(options)
4809            .network_config(network_config)
4810            .build();
4811        let network = TestNetwork::new(config, TEST_UPGRADE).await;
4812        client.connect(None).await;
4813
4814        // Fetch a network config from the API server. The first peer URL is bogus, to test the
4815        // failure/retry case.
4816        let peers = StatePeers::<StaticVersion<0, 1>>::from_urls(
4817            vec!["https://notarealnode.network".parse().unwrap(), url],
4818            Default::default(),
4819            Duration::from_secs(2),
4820            &NoMetrics,
4821        );
4822
4823        // Fetch the config from node 1, a different node than the one running the service.
4824        let validator =
4825            ValidatorConfig::generated_from_seed_indexed([0; 32], 1, U256::from(1), false);
4826        let config = peers.fetch_config(validator.clone()).await.unwrap();
4827
4828        // Check the node-specific information in the recovered config is correct.
4829        assert_eq!(config.node_index, 1);
4830
4831        // Check the public information is also correct (with respect to the node that actually
4832        // served the config, for public keys).
4833        pretty_assertions::assert_eq!(
4834            serde_json::to_value(PublicHotShotConfig::from(config.config)).unwrap(),
4835            serde_json::to_value(PublicHotShotConfig::from(
4836                network.cfg.hotshot_config().clone()
4837            ))
4838            .unwrap()
4839        );
4840    }
4841
4842    #[apply(sql_data_sources)]
4843    async fn test_pos_rewards_basic<D: TestableSqlDataSource>(
4844        _d: PhantomData<D>,
4845    ) -> anyhow::Result<()> {
4846        // A single validator that is also its only delegator leads every block, so the rewards
4847        // for an epoch, applied at the last block of the next one, all go to it.
4848        const EPOCH_HEIGHT: u64 = 10;
4849        const NUM_NODES: usize = 1;
4850
4851        let network_config = TestConfigBuilder::default()
4852            .epoch_height(EPOCH_HEIGHT)
4853            .build();
4854        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4855        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
4856
4857        let config = TestNetworkConfigBuilder::with_num_nodes()
4858            .api_config(D::options(&storage[0], Options::with_port(api_port)))
4859            .network_config(network_config.clone())
4860            .persistences(persistence.clone())
4861            .pos_hook(
4862                DelegationConfig::VariableAmounts,
4863                StakeTableContractVersion::V3,
4864                TEST_UPGRADE,
4865            )
4866            .await
4867            .unwrap()
4868            .build();
4869
4870        let network = TestNetwork::new(config, TEST_UPGRADE).await;
4871        let client: Client<ClientErr, SequencerApiVersion> =
4872            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
4873
4874        // The first rewards are for epoch 3, applied at the last block of epoch 4.
4875        let first_boundary = 4 * EPOCH_HEIGHT;
4876        wait_until_block_height(&client, "reward-state-v2/block-height", first_boundary).await;
4877
4878        let address = network_config.staking_priv_keys()[0].signer.address();
4879        let block_reward = network
4880            .server
4881            .node_state()
4882            .coordinator
4883            .membership()
4884            .epoch_block_reward(3.into())
4885            .expect("block reward is not None");
4886
4887        assert_eq!(
4888            reward_balance(&client, first_boundary - EPOCH_HEIGHT, address).await,
4889            U256::ZERO,
4890            "rewards before the first boundary"
4891        );
4892        assert_eq!(
4893            reward_balance(&client, first_boundary, address).await,
4894            U256::from(EPOCH_HEIGHT) * block_reward.0,
4895            "reward amount don't match"
4896        );
4897
4898        Ok(())
4899    }
4900
4901    #[apply(sql_data_sources)]
4902    async fn test_cumulative_pos_rewards<D: TestableSqlDataSource>(
4903        _d: PhantomData<D>,
4904    ) -> anyhow::Result<()> {
4905        // Five validators with several delegators each, one of which is also a validator. Across
4906        // every account, the balances grow only at an epoch's last block, and there by exactly
4907        // the previous epoch's total: its block reward for each of its blocks.
4908        const EPOCH_HEIGHT: u64 = 10;
4909        const NUM_NODES: usize = 5;
4910
4911        let network_config = TestConfigBuilder::default()
4912            .epoch_height(EPOCH_HEIGHT)
4913            .build();
4914        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4915        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
4916
4917        let config = TestNetworkConfigBuilder::with_num_nodes()
4918            .api_config(D::options(&storage[0], Options::with_port(api_port)))
4919            .network_config(network_config)
4920            .persistences(persistence.clone())
4921            .build();
4922
4923        let network = TestNetwork::new(config, TEST_UPGRADE).await;
4924        let node_state = network.server.node_state();
4925        let membership = node_state.coordinator.membership();
4926        let client: Client<ClientErr, SequencerApiVersion> =
4927            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
4928
4929        // The boundaries that apply the rewards for epochs 3 and 4.
4930        let boundaries = [4 * EPOCH_HEIGHT, 5 * EPOCH_HEIGHT];
4931        wait_until_block_height(&client, "reward-state-v2/block-height", boundaries[1]).await;
4932        let addresses = reward_addresses(&client, [3, 4]).await;
4933
4934        let mut prev_total = U256::ZERO;
4935        for height in [3 * EPOCH_HEIGHT].into_iter().chain(boundaries) {
4936            let mut total = U256::ZERO;
4937            for address in &addresses {
4938                total += reward_balance(&client, height, *address).await;
4939            }
4940            let epoch = epoch_from_block_number(height, EPOCH_HEIGHT);
4941            let expected = if epoch <= 3 {
4942                U256::ZERO
4943            } else {
4944                let block_reward = membership
4945                    .epoch_block_reward((epoch - 1).into())
4946                    .expect("block reward is not None");
4947                U256::from(EPOCH_HEIGHT) * block_reward.0
4948            };
4949            assert_eq!(total - prev_total, expected, "rewards applied at {height}");
4950            prev_total = total;
4951        }
4952
4953        Ok(())
4954    }
4955
4956    #[apply(sql_data_sources)]
4957    async fn test_epoch_rewards_per_account<D: TestableSqlDataSource>(
4958        _d: PhantomData<D>,
4959    ) -> anyhow::Result<()> {
4960        // Every account's balance matches the epoch's rewards computed from the leader counts
4961        // in the header of the epoch's last block: each leader gets its count times the block
4962        // reward, split between its commission and its delegators by stake. Also checks that
4963        // each validator's delegations sum to its stake, that the commission is within rounding
4964        // of its rate, and that the header totals what was distributed.
4965        const EPOCH_HEIGHT: u64 = 10;
4966        const NUM_NODES: usize = 5;
4967
4968        let network_config = TestConfigBuilder::default()
4969            .epoch_height(EPOCH_HEIGHT)
4970            .build();
4971        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
4972        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
4973
4974        let config = TestNetworkConfigBuilder::with_num_nodes()
4975            .api_config(D::options(&storage[0], Options::with_port(api_port)))
4976            .network_config(network_config)
4977            .persistences(persistence.clone())
4978            .build();
4979
4980        let network = TestNetwork::new(config, TEST_UPGRADE).await;
4981        let coordinator = network.server.node_state().coordinator;
4982        let client: Client<ClientErr, SequencerApiVersion> =
4983            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
4984
4985        wait_until_block_height(&client, "reward-state-v2/block-height", 5 * EPOCH_HEIGHT).await;
4986
4987        let mut expected = HashMap::<Address, U256>::new();
4988        let mut total_distributed = U256::ZERO;
4989        for epoch in [3, 4] {
4990            let header: Header = client
4991                .get(&format!("availability/header/{}", epoch * EPOCH_HEIGHT))
4992                .send()
4993                .await?;
4994            let leader_counts = *header
4995                .leader_counts()
4996                .expect("V6 header must have leader_counts");
4997            let snapshot = coordinator
4998                .membership()
4999                .snapshot(epoch.into())
5000                .expect("snapshot");
5001            let block_reward = snapshot.epoch_block_reward().expect("block reward");
5002
5003            for (validator, count) in
5004                ValidatorLeaderCounts::new(&snapshot, leader_counts)?.active_leaders()
5005            {
5006                let delegator_stake_sum: U256 = validator.delegators.values().cloned().sum();
5007                assert_eq!(delegator_stake_sum, validator.stake);
5008
5009                let validator_reward = block_reward.0 * U256::from(count);
5010                let computed = RewardDistributor::new(
5011                    validator.clone(),
5012                    RewardAmount(validator_reward),
5013                    Default::default(),
5014                )
5015                .compute_rewards()?;
5016
5017                let calculated_commission = U256::from(validator.commission)
5018                    .checked_mul(validator_reward)
5019                    .context("overflow")?
5020                    .checked_div(U256::from(COMMISSION_BASIS_POINTS))
5021                    .context("overflow")?;
5022                assert!(
5023                    computed.leader_commission().0 - calculated_commission <= U256::from(10_u64),
5024                    "commission of {} is off by more than rounding",
5025                    validator.account
5026                );
5027
5028                for (address, amount) in computed.all_rewards() {
5029                    *expected.entry(address).or_default() += amount.0;
5030                }
5031                total_distributed += validator_reward;
5032            }
5033
5034            let boundary = (epoch + 1) * EPOCH_HEIGHT;
5035            for (address, amount) in &expected {
5036                let balance = client
5037                    .get::<Option<RewardAmount>>(&format!(
5038                        "reward-state-v2/reward-balance/{boundary}/{address}"
5039                    ))
5040                    .send()
5041                    .await?
5042                    .unwrap_or_else(|| panic!("no reward for {address} at {boundary}"));
5043                assert_eq!(balance.0, *amount, "reward of {address} at {boundary}");
5044            }
5045            let boundary_header: Header = client
5046                .get(&format!("availability/header/{boundary}"))
5047                .send()
5048                .await?;
5049            assert_eq!(
5050                boundary_header.total_reward_distributed().unwrap().0,
5051                total_distributed,
5052                "total distributed at {boundary}"
5053            );
5054        }
5055
5056        Ok(())
5057    }
5058
5059    #[apply(sql_data_sources)]
5060    async fn test_stake_table_duplicate_events_from_contract<D: TestableSqlDataSource>(
5061        _d: PhantomData<D>,
5062    ) -> anyhow::Result<()> {
5063        // TODO(abdul): This test currently uses TestNetwork only for contract deployment and for L1 block number.
5064        // Once the stake table deployment logic is refactored and isolated, TestNetwork here will be unnecessary
5065
5066        let epoch_height = 20;
5067
5068        let network_config = TestConfigBuilder::default()
5069            .epoch_height(epoch_height)
5070            .build();
5071
5072        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
5073
5074        const NUM_NODES: usize = 5;
5075        // Initialize nodes.
5076        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
5077
5078        let l1_url = network_config.l1_url();
5079        let config = TestNetworkConfigBuilder::with_num_nodes()
5080            .api_config(D::options(&storage[0], Options::with_port(api_port)))
5081            .network_config(network_config)
5082            .persistences(persistence.clone())
5083            .build();
5084
5085        let network = TestNetwork::new(config, TEST_UPGRADE).await;
5086
5087        let mut prev_st = None;
5088        let state = network.server.decided_state().await.unwrap();
5089        let chain_config = state.chain_config.resolve().expect("resolve chain config");
5090        let stake_table = chain_config.stake_table_contract.unwrap();
5091
5092        let l1_client = L1ClientOptions::default()
5093            .connect(vec![l1_url])
5094            .expect("failed to connect to l1");
5095
5096        let client: Client<ClientErr, SequencerApiVersion> =
5097            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
5098
5099        let mut headers = client
5100            .socket("availability/stream/headers/0")
5101            .subscribe::<Header>()
5102            .await
5103            .unwrap();
5104
5105        let mut target_bh = 0;
5106        while let Some(header) = headers.next().await {
5107            let header = header.unwrap();
5108            println!("got header with height {}", header.height());
5109            if header.height() == 0 {
5110                continue;
5111            }
5112            let l1_block = header.l1_finalized().expect("l1 block not found");
5113
5114            let sorted_events = Fetcher::fetch_events_from_contract(
5115                l1_client.clone(),
5116                stake_table,
5117                None,
5118                l1_block.number(),
5119            )
5120            .await?;
5121
5122            let mut sorted_dedup_removed = sorted_events.clone();
5123            sorted_dedup_removed.dedup();
5124
5125            assert_eq!(
5126                sorted_events.len(),
5127                sorted_dedup_removed.len(),
5128                "duplicates found"
5129            );
5130
5131            // This also checks if there is a duplicate registration
5132            let stake_table =
5133                validators_from_l1_events(sorted_events.into_iter().map(|(_, e)| e)).unwrap();
5134            if let Some(prev_st) = prev_st {
5135                assert_eq!(stake_table, prev_st);
5136            }
5137
5138            prev_st = Some(stake_table);
5139
5140            if target_bh == 100 {
5141                break;
5142            }
5143
5144            target_bh = header.height();
5145        }
5146
5147        Ok(())
5148    }
5149
5150    /// Every validator account and delegator registered in these epochs.
5151    async fn reward_addresses(
5152        client: &Client<ClientErr, SequencerApiVersion>,
5153        epochs: impl IntoIterator<Item = u64>,
5154    ) -> HashSet<Address> {
5155        let mut addresses = HashSet::new();
5156        for epoch in epochs {
5157            let validators = client
5158                .get::<AuthenticatedValidatorMap>(&format!("node/validators/{epoch}"))
5159                .send()
5160                .await
5161                .expect("failed to get validators");
5162            for v in validators.values() {
5163                addresses.insert(v.account);
5164                addresses.extend(v.delegators.keys());
5165            }
5166        }
5167        addresses
5168    }
5169
5170    /// The node stores reward proofs only for the accounts in the tree, so an account without
5171    /// rewards at `height` is a 404 rather than a zero balance.
5172    async fn reward_balance(
5173        client: &Client<ClientErr, SequencerApiVersion>,
5174        height: u64,
5175        address: Address,
5176    ) -> U256 {
5177        match client
5178            .get::<RewardAmount>(&format!(
5179                "reward-state-v2/reward-balance/{height}/{address}"
5180            ))
5181            .send()
5182            .await
5183        {
5184            Ok(amount) => amount.0,
5185            Err(err) if err.status == StatusCode::NOT_FOUND => U256::ZERO,
5186            Err(err) => panic!("reward balance of {address} at {height}: {err}"),
5187        }
5188    }
5189
5190    /// From 0.3 to 0.5 every block paid its leader's commission and delegators, which only legacy
5191    /// consensus produced; the v4 chain's commissions change in epoch 2.
5192    #[apply(sql_data_sources)]
5193    async fn test_legacy_block_rewards<D: TestableSqlDataSource>(
5194        _d: PhantomData<D>,
5195    ) -> anyhow::Result<()> {
5196        let chain = LegacyChain::load("v4")?;
5197        let replay = chain.replay::<D>().await?;
5198        let client = &replay.client;
5199        let epoch_height = chain.epoch_height();
5200        let coordinator = replay.node.node_state().coordinator;
5201        let membership = coordinator.membership();
5202
5203        let mut expected = HashMap::<Address, U256>::new();
5204        let mut total_distributed = U256::ZERO;
5205        let mut validators_by_epoch = HashMap::<u64, AuthenticatedValidatorMap>::new();
5206        let mut paid_commissions = HashMap::<Address, HashSet<u16>>::new();
5207        for recorded in &chain.blocks[1..] {
5208            let leaf = recorded.leaf.leaf();
5209            let header = leaf.block_header();
5210            let height = header.height();
5211            let epoch = EpochNumber::new(epoch_from_block_number(height, epoch_height));
5212            if let std::collections::hash_map::Entry::Vacant(entry) =
5213                validators_by_epoch.entry(*epoch)
5214            {
5215                entry.insert(
5216                    client
5217                        .get(&format!("node/validators/{epoch}"))
5218                        .send()
5219                        .await?,
5220                );
5221            }
5222            let validators = &validators_by_epoch[&*epoch];
5223
5224            match membership.epoch_block_reward(epoch) {
5225                None => ensure!(*epoch <= 2, "epoch {epoch} pays no block reward"),
5226                Some(block_reward) => {
5227                    let snapshot = membership.snapshot(epoch).context("snapshot")?;
5228                    let leader = snapshot.leader(leaf.view_number()).expect("leader");
5229                    let account = snapshot
5230                        .validator_config(&leader)
5231                        .expect("leader config")
5232                        .account;
5233                    let leader = validators.get(&account).context("leader not found")?;
5234                    pay_block_reward(
5235                        &mut expected,
5236                        leader,
5237                        block_reward,
5238                        RewardAmount(total_distributed),
5239                    )?;
5240                    total_distributed += block_reward.0;
5241                    paid_commissions
5242                        .entry(account)
5243                        .or_default()
5244                        .insert(leader.commission);
5245                },
5246            }
5247            ensure!(
5248                header.total_reward_distributed().unwrap().0 == total_distributed,
5249                "total_reward_distributed at {height}"
5250            );
5251            assert_stakers_balances(client, height, validators, &expected).await?;
5252        }
5253        ensure!(!total_distributed.is_zero(), "the chain paid no rewards");
5254        ensure!(
5255            paid_commissions.values().any(|seen| seen.len() > 1),
5256            "no leader was paid at two different commissions"
5257        );
5258        Ok(())
5259    }
5260
5261    /// Adds what one block pays `leader`'s commission and delegators to `balances`.
5262    /// `distributed_before` is the chain's total before this block, as the parent header has it.
5263    fn pay_block_reward(
5264        balances: &mut HashMap<Address, U256>,
5265        leader: &AuthenticatedValidator<PubKey>,
5266        block_reward: RewardAmount,
5267        distributed_before: RewardAmount,
5268    ) -> anyhow::Result<()> {
5269        let delegated: U256 = leader.delegators.values().cloned().sum();
5270        ensure!(delegated == leader.stake, "delegations add up to the stake");
5271
5272        let rewards = RewardDistributor::new(leader.clone(), block_reward, distributed_before)
5273            .compute_rewards()?;
5274        let commission =
5275            U256::from(leader.commission) * block_reward.0 / U256::from(COMMISSION_BASIS_POINTS);
5276        // The leader also gets what flooring each delegator's share left over.
5277        ensure!(
5278            rewards.leader_commission().0 - commission <= U256::from(leader.delegators.len()),
5279            "commission {} is not {commission}",
5280            rewards.leader_commission().0
5281        );
5282
5283        for (address, amount) in rewards.delegators().clone() {
5284            *balances.entry(address).or_default() += amount.0;
5285        }
5286        *balances.entry(leader.account).or_default() += rewards.leader_commission().0;
5287        Ok(())
5288    }
5289
5290    /// Checks every validator's and delegator's balance in the v2 reward tree at `height`.
5291    async fn assert_stakers_balances(
5292        client: &Client<ClientErr, SequencerApiVersion>,
5293        height: u64,
5294        validators: &AuthenticatedValidatorMap,
5295        expected: &HashMap<Address, U256>,
5296    ) -> anyhow::Result<()> {
5297        let stakers = validators
5298            .values()
5299            .flat_map(|v| v.delegators.keys().copied().chain([v.account]))
5300            .chain(expected.keys().copied())
5301            .collect::<HashSet<_>>();
5302        for staker in stakers {
5303            let balance = match client
5304                .get::<RewardAmount>(&format!("reward-state-v2/reward-balance/{height}/{staker}"))
5305                .send()
5306                .await
5307            {
5308                Ok(amount) => Some(amount.0),
5309                // Not in the tree yet.
5310                Err(err) if err.status == StatusCode::NOT_FOUND => None,
5311                Err(err) => bail!("reward balance of {staker} at {height}: {err}"),
5312            };
5313            ensure!(
5314                balance == expected.get(&staker).copied(),
5315                "balance of {staker} at {height}: {balance:?}, expected {:?}",
5316                expected.get(&staker)
5317            );
5318        }
5319        Ok(())
5320    }
5321
5322    /// Stakers claim rewards earned before 0.5 with proofs against those blocks' v2 reward roots.
5323    #[apply(sql_data_sources)]
5324    async fn test_legacy_reward_queries<D: TestableSqlDataSource>(
5325        _d: PhantomData<D>,
5326    ) -> anyhow::Result<()> {
5327        let chain = LegacyChain::load("v4")?;
5328        let replay = chain.replay::<D>().await?;
5329        let client = &replay.client;
5330        let tip = chain.tip();
5331        let root = chain
5332            .blocks
5333            .last()
5334            .unwrap()
5335            .leaf
5336            .header()
5337            .reward_merkle_tree_root();
5338        let root = root
5339            .right()
5340            .context("v4 headers commit to the v2 reward tree")?;
5341
5342        let stakers = all_stakers(client, &chain).await?;
5343        let mut balances = vec![];
5344        for staker in stakers {
5345            let res: RewardAccountQueryDataV2 = client
5346                .get(&format!("reward-state-v2/proof/{tip}/{staker}"))
5347                .send()
5348                .await?;
5349            ensure!(res.proof.verify(&root)? == res.balance, "proof of {staker}");
5350            if res.balance.is_zero() {
5351                continue;
5352            }
5353            let claim: RewardClaimInput = client
5354                .get(&format!(
5355                    "reward-state-v2/reward-claim-input/{tip}/{staker}"
5356                ))
5357                .send()
5358                .await?;
5359            ensure!(
5360                claim.lifetime_rewards == res.balance,
5361                "claim of {staker} pays its proven balance"
5362            );
5363            // The contract rebuilds the root from these siblings, so they must be the proof's.
5364            let siblings: RewardProofSiblings = res.proof.clone().try_into()?;
5365            ensure!(
5366                RewardAuthData::try_from(claim.auth_data)? == RewardAuthData::new(siblings),
5367                "claim of {staker} carries its proof"
5368            );
5369            balances.push((RewardAccountV2(staker), RewardAmount(res.balance)));
5370        }
5371        ensure!(!balances.is_empty(), "the chain paid no rewards");
5372
5373        balances.sort_by_key(|(account, _)| std::cmp::Reverse(*account));
5374        let page: Vec<(RewardAccountV2, RewardAmount)> = client
5375            .get(&format!("reward-state-v2/reward-amounts/{tip}/0/10000"))
5376            .send()
5377            .await?;
5378        ensure!(page == balances, "reward page {page:?}");
5379
5380        let coordinator = replay.node.node_state().coordinator;
5381        for epoch in 3..=chain.last_epoch() {
5382            let reward: Option<RewardAmount> = client
5383                .get(&format!("node/block-reward/epoch/{epoch}"))
5384                .send()
5385                .await?;
5386            let expected = coordinator
5387                .membership()
5388                .epoch_block_reward(EpochNumber::new(epoch));
5389            ensure!(reward == expected, "block reward of epoch {epoch}");
5390        }
5391        Ok(())
5392    }
5393
5394    /// Every validator and delegator of every epoch of `chain`.
5395    async fn all_stakers(
5396        client: &Client<ClientErr, SequencerApiVersion>,
5397        chain: &LegacyChain,
5398    ) -> anyhow::Result<HashSet<Address>> {
5399        let last_epoch = chain.last_epoch();
5400        let mut stakers = HashSet::new();
5401        for epoch in 1..=last_epoch {
5402            let validators: AuthenticatedValidatorMap = client
5403                .get(&format!("node/validators/{epoch}"))
5404                .send()
5405                .await?;
5406            for v in validators.values() {
5407                stakers.insert(v.account);
5408                stakers.extend(v.delegators.keys());
5409            }
5410        }
5411        Ok(stakers)
5412    }
5413
5414    #[apply(sql_data_sources)]
5415    async fn test_epoch_reward_distribution_basic<D: TestableSqlDataSource>(
5416        _d: PhantomData<D>,
5417    ) -> anyhow::Result<()> {
5418        const EPOCH_HEIGHT: u64 = 10;
5419        const NUM_NODES: usize = 5;
5420
5421        let network_config = TestConfigBuilder::default()
5422            .epoch_height(EPOCH_HEIGHT)
5423            .build();
5424
5425        let api_port = reserve_tcp_port().expect("No ports free for query service");
5426
5427        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
5428
5429        let config = TestNetworkConfigBuilder::with_num_nodes()
5430            .api_config(D::options(&storage[0], Options::with_port(api_port)))
5431            .network_config(network_config)
5432            .persistences(persistence.clone())
5433            .build();
5434
5435        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
5436        let client: Client<ClientErr, SequencerApiVersion> =
5437            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
5438
5439        // Wait for chain to reach epoch 5
5440        let height_client: Client<ClientErr, StaticVersion<0, 1>> =
5441            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
5442        wait_until_block_height(&height_client, "node/block-height", EPOCH_HEIGHT * 5).await;
5443
5444        let mut leaves = client
5445            .socket("availability/stream/leaves/0")
5446            .subscribe::<LeafQueryData<SeqTypes>>()
5447            .await
5448            .unwrap();
5449
5450        // Epochs 1-3: verify no rewards
5451        while let Some(leaf) = leaves.next().await {
5452            let leaf = leaf.unwrap();
5453            let header = leaf.header();
5454            let height = header.height();
5455
5456            let total_distributed = header.total_reward_distributed().unwrap();
5457            assert_eq!(
5458                total_distributed.0,
5459                U256::ZERO,
5460                "epochs 1-3 should have no rewards, height={height}"
5461            );
5462
5463            if height == EPOCH_HEIGHT * 3 {
5464                break;
5465            }
5466        }
5467
5468        while let Some(leaf) = leaves.next().await {
5469            let leaf = leaf.unwrap();
5470            let header = leaf.header();
5471            let height = header.height();
5472
5473            if height == EPOCH_HEIGHT * 4 {
5474                let total_distributed = header.total_reward_distributed().unwrap();
5475                assert!(total_distributed.0 > U256::ZERO,);
5476                break;
5477            }
5478        }
5479
5480        while let Some(leaf) = leaves.next().await {
5481            let leaf = leaf.unwrap();
5482            let header = leaf.header();
5483            let height = header.height();
5484
5485            if height == EPOCH_HEIGHT * 5 {
5486                let total_distributed = header.total_reward_distributed().unwrap();
5487                assert!(total_distributed.0 > U256::ZERO,);
5488                break;
5489            }
5490        }
5491
5492        Ok(())
5493    }
5494
5495    /// Run a `TestNetwork` based directly on the new protocol version (V0_6,
5496    /// no upgrade/cutover) and verify it produces blocks from genesis.
5497    #[apply(sql_data_sources)]
5498    async fn test_new_protocol_produces_blocks<D: TestableSqlDataSource>(
5499        _d: PhantomData<D>,
5500    ) -> anyhow::Result<()> {
5501        const EPOCH_HEIGHT: u64 = 100;
5502        const NUM_NODES: usize = 5;
5503        const TARGET_BLOCK_HEIGHT: u64 = 100;
5504
5505        let network_config = TestConfigBuilder::default()
5506            .epoch_height(EPOCH_HEIGHT)
5507            .epoch_start_block(0)
5508            .build();
5509
5510        let api_port = reserve_tcp_port().expect("No ports free for query service");
5511
5512        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
5513
5514        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
5515            .api_config(D::options(&storage[0], Options::with_port(api_port)))
5516            .network_config(network_config)
5517            .persistences(persistence)
5518            .build();
5519
5520        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
5521
5522        let client: Client<ClientErr, SequencerApiVersion> =
5523            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
5524        client.connect(Some(Duration::from_secs(30))).await;
5525
5526        let mut leaves = client
5527            .socket("availability/stream/leaves/0")
5528            .subscribe::<LeafQueryData<SeqTypes>>()
5529            .await
5530            .expect("subscribe to leaf stream");
5531
5532        let mut height = 0;
5533        while let Some(leaf) = leaves.next().await {
5534            let leaf = leaf.expect("leaf stream yielded an error");
5535            let header = leaf.header();
5536            height = header.height();
5537
5538            if height > 0 {
5539                assert_eq!(
5540                    header.version(),
5541                    NEW_PROTOCOL_VERSION,
5542                    "block {height} should be produced under the new protocol version",
5543                );
5544            }
5545
5546            if height >= TARGET_BLOCK_HEIGHT {
5547                break;
5548            }
5549        }
5550
5551        assert!(
5552            height >= TARGET_BLOCK_HEIGHT,
5553            "expected at least {TARGET_BLOCK_HEIGHT} blocks, got {height} (leaf stream ended \
5554             early)",
5555        );
5556
5557        Ok(())
5558    }
5559
5560    /// Run a network on the new protocol from genesis (V0_6) through the
5561    /// LargeBlock (V0_7) upgrade, whose chain config takes effect with the
5562    /// version bump.
5563    #[apply(sql_data_sources)]
5564    async fn test_new_protocol_large_block_upgrade<D: TestableSqlDataSource>(
5565        _d: PhantomData<D>,
5566    ) -> anyhow::Result<()> {
5567        const EPOCH_HEIGHT: u64 = 10;
5568        const NUM_NODES: usize = 5;
5569        const UPGRADE_START_PROPOSING_VIEW: u64 = 30;
5570        const UPGRADE: Upgrade = Upgrade::new(NEW_PROTOCOL_VERSION, LARGE_BLOCK_VERSION);
5571
5572        let api_port = reserve_tcp_port().expect("No ports free for query service");
5573        let url: Url = format!("http://localhost:{api_port}").parse().unwrap();
5574
5575        let test_config = TestConfigBuilder::<NUM_NODES>::default()
5576            .epoch_height(EPOCH_HEIGHT)
5577            .epoch_start_block(0)
5578            .set_upgrades(LARGE_BLOCK_VERSION)
5579            .await
5580            .upgrade_proposing_views(UPGRADE_START_PROPOSING_VIEW, 1000)
5581            .build();
5582
5583        test_config
5584            .anvil()
5585            .expect("TestConfigBuilder starts an anvil")
5586            .anvil_set_interval_mining(1)
5587            .await
5588            .expect("interval mining");
5589
5590        // The genesis chain config differs from the upgrade's only in
5591        // `max_block_size`.
5592        let upgrade_chain_config = test_config
5593            .get_upgrade_map()
5594            .chain_config(LARGE_BLOCK_VERSION);
5595        let genesis_chain_config = ChainConfig {
5596            max_block_size: ChainConfig::default().max_block_size,
5597            ..upgrade_chain_config
5598        };
5599        assert_ne!(
5600            genesis_chain_config.max_block_size,
5601            upgrade_chain_config.max_block_size
5602        );
5603        let genesis_state = ValidatedState {
5604            chain_config: genesis_chain_config.into(),
5605            ..Default::default()
5606        };
5607
5608        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
5609
5610        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
5611            .api_config(D::options(&storage[0], Options::with_port(api_port)))
5612            .persistences(persistence)
5613            .states(std::array::from_fn(|_| genesis_state.clone()))
5614            .catchups(std::array::from_fn(|_| {
5615                StatePeers::<SequencerApiVersion>::from_urls(
5616                    vec![url.clone()],
5617                    Default::default(),
5618                    Duration::from_secs(2),
5619                    &NoMetrics,
5620                )
5621            }))
5622            .network_config(test_config)
5623            .build();
5624
5625        let network = TestNetwork::new(config, UPGRADE).await;
5626        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
5627        client.connect(None).await;
5628
5629        let mut leaves = client
5630            .socket("availability/stream/leaves/0")
5631            .subscribe::<LeafQueryData<SeqTypes>>()
5632            .await
5633            .unwrap();
5634
5635        // Activation is at the upgrade proposal's view plus
5636        // `hotshot_new_protocol::upgrade::FINISH_OFFSET` (20).
5637        let upgrade_height = timeout(Duration::from_secs(600), async {
5638            loop {
5639                let leaf = leaves.next().await.unwrap().unwrap();
5640                if leaf.header().version() >= LARGE_BLOCK_VERSION {
5641                    assert_eq!(
5642                        leaf.header().chain_config().commit(),
5643                        upgrade_chain_config.commit(),
5644                        "upgraded header must commit to the upgraded chain config"
5645                    );
5646                    break leaf.height();
5647                }
5648                assert_eq!(
5649                    leaf.header().chain_config().commit(),
5650                    genesis_chain_config.commit(),
5651                    "pre-upgrade header must commit to the genesis chain config"
5652                );
5653                tracing::info!(
5654                    version = %leaf.header().version(),
5655                    height = leaf.header().height(),
5656                    view = ?leaf.leaf().view_number(),
5657                    "waiting for the large-block upgrade"
5658                );
5659            }
5660        })
5661        .await
5662        .expect("the network did not upgrade to the large-block version");
5663        tracing::info!(upgrade_height, "large-block upgrade complete");
5664
5665        timeout(Duration::from_secs(120), async {
5666            loop {
5667                let leaf = leaves.next().await.unwrap().unwrap();
5668                assert!(leaf.header().version() >= LARGE_BLOCK_VERSION);
5669                if leaf.height() > upgrade_height + 3 {
5670                    break;
5671                }
5672            }
5673        })
5674        .await
5675        .expect("the network stalled after the upgrade");
5676
5677        for node in &network.peers {
5678            let state = node
5679                .decided_state()
5680                .await
5681                .expect("node has a decided state");
5682            assert_eq!(
5683                state
5684                    .chain_config
5685                    .resolve()
5686                    .expect("decided state resolves its chain config"),
5687                upgrade_chain_config,
5688            );
5689        }
5690
5691        Ok(())
5692    }
5693
5694    /// Full-application epoch-boundary committee change: a validator sends a
5695    /// real `deregisterValidator` transaction to the StakeTable contract on
5696    /// L1 mid-run, drops out of the consensus committee at the epoch boundary
5697    /// where the exit activates, and the reduced committee keeps deciding.
5698    ///
5699    /// Runs genesis at `NEW_PROTOCOL_VERSION` (StakeTable V3), so this covers
5700    /// the whole pipeline the HotShot-level tests in
5701    /// `hotshot-new-protocol/src/tests/stake_table_changes.rs` mock out:
5702    /// L1 event fetching, `select_active_validator_set`, and epoch-root-driven
5703    /// membership updates.
5704    #[apply(sql_data_sources)]
5705    async fn test_new_protocol_validator_exit_at_epoch_boundary<D: TestableSqlDataSource>(
5706        _d: PhantomData<D>,
5707    ) -> anyhow::Result<()> {
5708        const NUM_NODES: usize = 5;
5709        const EPOCH_HEIGHT: u64 = 10;
5710        /// How many epochs after the exit transaction we allow for the event
5711        /// to finalize on L1 and reach a stake table snapshot before failing.
5712        const MAX_ACTIVATION_EPOCHS: u64 = 10;
5713
5714        let network_config = TestConfigBuilder::default()
5715            .epoch_height(EPOCH_HEIGHT)
5716            .epoch_start_block(0)
5717            .build();
5718
5719        let api_port = reserve_tcp_port().expect("No ports free for query service");
5720
5721        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
5722
5723        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
5724            .api_config(D::options(&storage[0], Options::with_port(api_port)))
5725            .network_config(network_config.clone())
5726            .persistences(persistence)
5727            .build();
5728
5729        let network = TestNetwork::new(config, TEST_UPGRADE).await;
5730        let st_addr = network
5731            .contracts
5732            .as_ref()
5733            .unwrap()
5734            .address(Contract::StakeTableProxy)
5735            .unwrap();
5736
5737        // Exit the last node's validator. The node keeps running (matching
5738        // the HotShot-level leave test), but must disappear from the
5739        // committee. Node 0 serves the query API, so it stays a member.
5740        let (exiting, exiting_provider) = network_config
5741            .validator_providers()
5742            .pop()
5743            .expect("at least one validator");
5744
5745        let client: Client<ClientErr, SequencerApiVersion> =
5746            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
5747        client.connect(Some(Duration::from_secs(30))).await;
5748
5749        // Let the network settle into normal epoch operation before exiting.
5750        let mut events = network.peers[0].event_stream();
5751        wait_for_epochs(&mut events, EPOCH_HEIGHT, 3).await;
5752        let exit_epoch = network.peers[0]
5753            .decided_leaf()
5754            .await
5755            .epoch(EPOCH_HEIGHT)
5756            .unwrap()
5757            .u64();
5758
5759        let validators = client
5760            .get::<AuthenticatedValidatorMap>(&format!("node/validators/{exit_epoch}"))
5761            .send()
5762            .await
5763            .expect("validators for the exit epoch");
5764        assert!(
5765            validators.contains_key(&exiting),
5766            "exiting validator should be in the committee before its exit activates"
5767        );
5768        assert_eq!(validators.len(), NUM_NODES);
5769
5770        let receipt = StakingTransaction::DeregisterValidator {
5771            stake_table: st_addr,
5772        }
5773        .send(&exiting_provider)
5774        .await?
5775        .get_receipt()
5776        .await?;
5777        assert!(receipt.status(), "deregistration transaction reverted");
5778
5779        // The exit activates at the first epoch whose stake table snapshot is
5780        // taken after the event finalizes on L1; scan forward until it does.
5781        let mut activation_epoch = None;
5782        for epoch in exit_epoch + 1..=exit_epoch + MAX_ACTIVATION_EPOCHS {
5783            wait_for_epochs(&mut events, EPOCH_HEIGHT, epoch).await;
5784            let validators = client
5785                .get::<AuthenticatedValidatorMap>(&format!("node/validators/{epoch}"))
5786                .send()
5787                .await
5788                .expect("validators for a decided epoch");
5789            if !validators.contains_key(&exiting) {
5790                assert_eq!(
5791                    validators.len(),
5792                    NUM_NODES - 1,
5793                    "only the exited validator should have left the committee"
5794                );
5795                activation_epoch = Some(epoch);
5796                break;
5797            }
5798            assert_eq!(validators.len(), NUM_NODES);
5799        }
5800        let activation_epoch = activation_epoch.unwrap_or_else(|| {
5801            panic!("validator still in the committee {MAX_ACTIVATION_EPOCHS} epochs after exiting")
5802        });
5803
5804        // The reduced committee must keep deciding across further epoch
5805        // boundaries.
5806        wait_for_epochs(&mut events, EPOCH_HEIGHT, activation_epoch + 2).await;
5807
5808        Ok(())
5809    }
5810
5811    const FOLLOW_TIMEOUT: Duration = Duration::from_secs(240);
5812
5813    /// Node 0 is the follower's only upstream. The query service only stores new heights through
5814    /// the follow loop, so the follower's archive reaching the validators' tip proves it follows
5815    /// live; then the endpoints a staked query node serves are compared with node 0's.
5816    #[apply(sql_data_sources)]
5817    async fn test_light_client_follower_query_node<D: TestableSqlDataSource>(
5818        _d: PhantomData<D>,
5819    ) -> anyhow::Result<()> {
5820        const NUM_NODES: usize = 5;
5821        const EPOCH_HEIGHT: u64 = 10;
5822        const EPOCHS_TO_FOLLOW: u64 = 4;
5823        const NEW_PROTOCOL: Upgrade = Upgrade::trivial(NEW_PROTOCOL_VERSION);
5824
5825        let network_config = TestConfigBuilder::default()
5826            .epoch_height(EPOCH_HEIGHT)
5827            .epoch_start_block(0)
5828            .build();
5829
5830        let api_port = reserve_tcp_port().expect("No ports free for query service");
5831        let api_url: Url = format!("http://localhost:{api_port}").parse()?;
5832
5833        let storage = join_all((0..NUM_NODES).map(|_| D::create_storage())).await;
5834        let persistence: [_; NUM_NODES] = storage
5835            .iter()
5836            .map(<D as TestableSequencerDataSource>::persistence_options)
5837            .collect::<Vec<_>>()
5838            .try_into()
5839            .unwrap();
5840        let follower_storage = D::create_storage().await;
5841
5842        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
5843            .api_config(
5844                Options::with_port(api_port)
5845                    .catchup(Default::default())
5846                    .config(Default::default())
5847                    .submit(Default::default())
5848                    .query_sql(Query::default(), D::tmp_options(&storage[0])),
5849            )
5850            .network_config(network_config)
5851            .persistences(persistence)
5852            .catchups(std::array::from_fn(|_| {
5853                StatePeers::<SequencerApiVersion>::from_urls(
5854                    vec![api_url.clone()],
5855                    Default::default(),
5856                    Duration::from_secs(2),
5857                    &NoMetrics,
5858                )
5859            }))
5860            .pos_hook(
5861                DelegationConfig::MultipleDelegators,
5862                StakeTableContractVersion::V3,
5863                NEW_PROTOCOL,
5864            )
5865            .await?
5866            .build();
5867
5868        let genesis_state = config.states()[0].clone();
5869        let network = TestNetwork::new(config, NEW_PROTOCOL).await;
5870        let api_client: Client<ClientErr, SequencerApiVersion> = Client::new(api_url.clone());
5871        assert!(
5872            api_client.connect(Some(Duration::from_secs(60))).await,
5873            "node 0 query API did not come up"
5874        );
5875
5876        // Let the validators decide the first epoch before the follower bootstraps from them.
5877        let mut events = network.server.event_stream();
5878        wait_for_epochs(&mut events, EPOCH_HEIGHT, 1).await;
5879
5880        let genesis = follower_genesis(&network.cfg, &genesis_state, NEW_PROTOCOL);
5881        let params = FollowerParams {
5882            upstreams: vec![api_url.clone()],
5883            config_peers: None,
5884            catchup: CatchupParams {
5885                state_peers: vec![api_url.clone()],
5886                backoff: Default::default(),
5887                base_timeout: Duration::from_secs(2),
5888                local_timeout: Duration::from_secs(5),
5889                http_delay: Duration::ZERO,
5890            },
5891            bootstrap_epoch_catchup_timeout: Duration::from_secs(30),
5892            options: FollowerOptions {
5893                poll_interval: Duration::from_millis(500),
5894                max_blocks_per_poll: 10,
5895                light_client: Default::default(),
5896                light_client_db: Default::default(),
5897                light_client_genesis: None,
5898            },
5899        };
5900        let l1_params = || L1Params {
5901            urls: vec![network.cfg.l1_url()],
5902            options: network.cfg.l1_opt(),
5903        };
5904        let (mut follower, follower_client) = start_follower::<D>(
5905            &follower_storage,
5906            genesis.clone(),
5907            params.clone(),
5908            l1_params(),
5909        )
5910        .await?;
5911
5912        wait_for_epochs(&mut events, EPOCH_HEIGHT, EPOCHS_TO_FOLLOW).await;
5913        let tip = network.server.decided_leaf().await.height();
5914        assert_follows_to(&follower, &follower_client, tip).await?;
5915
5916        let finalized_height = latest_cert2_height(&api_client, tip)
5917            .await
5918            .context("no cert2 stored on a new protocol chain")?;
5919        assert_same_chain(&api_client, &follower_client, finalized_height).await?;
5920        assert_same_epoch_state(
5921            &api_client,
5922            &follower_client,
5923            epoch_from_block_number(tip, EPOCH_HEIGHT),
5924        )
5925        .await?;
5926        assert_same_merklized_state(
5927            &api_client,
5928            &follower_client,
5929            finalized_height,
5930            TestConfig::<NUM_NODES>::builder_key().fee_account(),
5931            genesis_state.chain_config.commit(),
5932        )
5933        .await?;
5934        let rewarded: Vec<RewardAccountV2> = network
5935            .server
5936            .decided_state()
5937            .await
5938            .unwrap()
5939            .reward_merkle_tree_v2
5940            .iter()
5941            .map(|(account, _)| *account)
5942            .collect();
5943        assert!(
5944            !rewarded.is_empty(),
5945            "no rewards distributed after {EPOCHS_TO_FOLLOW} epochs"
5946        );
5947        assert_same_rewards(&api_client, &follower_client, tip, &rewarded).await?;
5948
5949        let txn = Transaction::new(NamespaceId::from(1_u32), vec![1, 2, 3]);
5950        let hash: Commitment<Transaction> = follower_client
5951            .post("submit/submit")
5952            .body_binary(&txn)?
5953            .send()
5954            .await
5955            .context("follower did not forward the transaction")?;
5956        assert_eq!(hash, txn.commit());
5957        wait_for_decide_on_handle(&mut events, &txn).await;
5958
5959        assert_same_response(&api_client, &follower_client, "config/hotshot").await?;
5960        follower_client
5961            .get::<LeafProof>(&format!("light-client/leaf/{finalized_height}"))
5962            .send()
5963            .await
5964            .context("follower does not serve light client proofs")?;
5965        assert_validator_only_routes_not_found(&follower_client, finalized_height).await;
5966
5967        // Only one node may write to the database, so the first follower stops before the second
5968        // starts.
5969        follower.shut_down().await;
5970        drop(follower);
5971        let (_follower, follower_client) =
5972            start_follower::<D>(&follower_storage, genesis, params, l1_params()).await?;
5973        wait_for_epochs(&mut events, EPOCH_HEIGHT, EPOCHS_TO_FOLLOW + 1).await;
5974        let tip = network.server.decided_leaf().await.height();
5975        timeout(
5976            FOLLOW_TIMEOUT,
5977            wait_until_block_height(&follower_client, "node/block-height", tip + 1),
5978        )
5979        .await
5980        .context("restarted follower did not catch up with the validators")?;
5981
5982        Ok(())
5983    }
5984
5985    /// Mirrors what `TestConfig::init_node` gives its validators.
5986    fn follower_genesis<const N: usize>(
5987        cfg: &TestConfig<N>,
5988        genesis_state: &ValidatedState,
5989        upgrade: Upgrade,
5990    ) -> NodeGenesis {
5991        let hotshot = cfg.hotshot_config();
5992        let chain_config = genesis_state
5993            .chain_config
5994            .resolve()
5995            .expect("test states carry a full chain config");
5996        NodeGenesis {
5997            base_version: upgrade.base,
5998            upgrade_version: upgrade.target,
5999            genesis_version: upgrade.base,
6000            epoch_height: Some(hotshot.epoch_height),
6001            drb_difficulty: Some(hotshot.drb_difficulty),
6002            drb_upgrade_difficulty: Some(hotshot.drb_upgrade_difficulty),
6003            epoch_start_block: Some(hotshot.epoch_start_block),
6004            stake_table_capacity: Some(hotshot.stake_table_capacity),
6005            chain_config,
6006            stake_table: StakeTableConfig {
6007                capacity: hotshot.stake_table_capacity,
6008            },
6009            accounts: [(
6010                TestConfig::<N>::builder_key().fee_account(),
6011                U256::MAX.into(),
6012            )]
6013            .into_iter()
6014            .collect(),
6015            l1_finalized: L1Finalized::Number { number: 0 },
6016            header: GenesisHeader {
6017                chain_config,
6018                ..Default::default()
6019            },
6020            upgrades: cfg.upgrades(),
6021            da_committees: None,
6022        }
6023    }
6024
6025    async fn start_follower<D: TestableSqlDataSource>(
6026        storage: &<D as TestableSequencerDataSource>::Storage,
6027        genesis: NodeGenesis,
6028        params: FollowerParams,
6029        l1_params: L1Params,
6030    ) -> anyhow::Result<(
6031        FollowerContext<<D::Options as PersistenceOptions>::Persistence>,
6032        Client<ClientErr, SequencerApiVersion>,
6033    )> {
6034        let port = reserve_tcp_port().expect("No ports free for query service");
6035        let mut db = D::persistence_options(storage);
6036        let persistence = db.create().await?;
6037        let handle = db
6038            .enable_query_module(
6039                Options::with_port(port),
6040                Query {
6041                    peers: params.upstreams.clone(),
6042                    ..Default::default()
6043                },
6044            )
6045            .catchup(Default::default())
6046            .config(Default::default())
6047            .submit(Default::default())
6048            .serve(move |metrics, sink, _| {
6049                async move {
6050                    init_follower_node(genesis, params, metrics, persistence, l1_params, sink).await
6051                }
6052                .boxed()
6053            })
6054            .await
6055            .context("follower should start")?;
6056        let follower = FollowerContext::new(handle);
6057
6058        let client: Client<ClientErr, SequencerApiVersion> =
6059            Client::new(format!("http://localhost:{port}").parse()?);
6060        ensure!(
6061            client.connect(Some(Duration::from_secs(60))).await,
6062            "follower query API did not come up"
6063        );
6064        Ok((follower, client))
6065    }
6066
6067    async fn assert_follows_to<P: SequencerPersistence>(
6068        follower: &FollowerContext<P>,
6069        follower_client: &Client<ClientErr, SequencerApiVersion>,
6070        tip: u64,
6071    ) -> anyhow::Result<()> {
6072        timeout(FOLLOW_TIMEOUT, async {
6073            while follower.decided_leaf().await.height() < tip {
6074                sleep(Duration::from_secs(1)).await;
6075            }
6076        })
6077        .await
6078        .context("follower did not decide up to the validators' tip")?;
6079        // `node/block-height` counts blocks, so `tip` is stored at `tip + 1`.
6080        timeout(
6081            FOLLOW_TIMEOUT,
6082            wait_until_block_height(follower_client, "node/block-height", tip + 1),
6083        )
6084        .await
6085        .context("follower's archive did not reach the validators' tip")
6086    }
6087
6088    /// Cert2s are only stored at the heights they finalize.
6089    async fn latest_cert2_height(
6090        client: &Client<ClientErr, SequencerApiVersion>,
6091        tip: u64,
6092    ) -> Option<u64> {
6093        for height in (1..tip).rev() {
6094            if client
6095                .get::<Certificate2<SeqTypes>>(&format!("availability/cert2/{height}"))
6096                .send()
6097                .await
6098                .is_ok()
6099            {
6100                return Some(height);
6101            }
6102        }
6103        None
6104    }
6105
6106    async fn assert_same_chain(
6107        api_client: &Client<ClientErr, SequencerApiVersion>,
6108        follower_client: &Client<ClientErr, SequencerApiVersion>,
6109        height: u64,
6110    ) -> anyhow::Result<()> {
6111        for path in [
6112            format!("availability/leaf/{height}"),
6113            format!("availability/header/{height}"),
6114            format!("availability/block/{height}"),
6115            format!("availability/vid/common/{height}"),
6116            format!("availability/cert2/{height}"),
6117            format!("catchup/{height}/cert2"),
6118        ] {
6119            assert_same_response(api_client, follower_client, &path).await?;
6120        }
6121        Ok(())
6122    }
6123
6124    async fn assert_same_epoch_state(
6125        api_client: &Client<ClientErr, SequencerApiVersion>,
6126        follower_client: &Client<ClientErr, SequencerApiVersion>,
6127        current_epoch: u64,
6128    ) -> anyhow::Result<()> {
6129        for epoch in 1..=current_epoch {
6130            for path in [
6131                format!("node/stake-table/{epoch}"),
6132                format!("node/validators/{epoch}"),
6133                format!("node/block-reward/epoch/{epoch}"),
6134            ] {
6135                assert_same_response(api_client, follower_client, &path).await?;
6136            }
6137            let state_cert = format!("availability/state-cert-v2/{epoch}");
6138            if api_client
6139                .get::<serde_json::Value>(&state_cert)
6140                .send()
6141                .await
6142                .is_ok()
6143            {
6144                assert_same_response(api_client, follower_client, &state_cert).await?;
6145            }
6146        }
6147        Ok(())
6148    }
6149
6150    async fn assert_same_merklized_state(
6151        api_client: &Client<ClientErr, SequencerApiVersion>,
6152        follower_client: &Client<ClientErr, SequencerApiVersion>,
6153        height: u64,
6154        account: FeeAccount,
6155        chain_config: Commitment<ChainConfig>,
6156    ) -> anyhow::Result<()> {
6157        for client in [api_client, follower_client] {
6158            timeout(
6159                FOLLOW_TIMEOUT,
6160                wait_until_block_height(client, "fee-state/block-height", height + 1),
6161            )
6162            .await
6163            .context("merklized state did not reach the finalized height")?;
6164        }
6165        let leaf: LeafQueryData<SeqTypes> = api_client
6166            .get(&format!("availability/leaf/{height}"))
6167            .send()
6168            .await?;
6169        let view = leaf.leaf().view_number();
6170        for path in [
6171            format!("fee-state/{height}/{account}"),
6172            format!("catchup/{height}/{view}/account/{account}"),
6173            format!("catchup/{height}/{view}/blocks"),
6174            format!("catchup/chain-config/{chain_config}"),
6175        ] {
6176            assert_same_response(api_client, follower_client, &path).await?;
6177        }
6178        Ok(())
6179    }
6180
6181    async fn assert_same_rewards(
6182        api_client: &Client<ClientErr, SequencerApiVersion>,
6183        follower_client: &Client<ClientErr, SequencerApiVersion>,
6184        tip: u64,
6185        accounts: &[RewardAccountV2],
6186    ) -> anyhow::Result<()> {
6187        for client in [api_client, follower_client] {
6188            timeout(
6189                FOLLOW_TIMEOUT,
6190                wait_until_block_height(client, "reward-state-v2/block-height", tip + 1),
6191            )
6192            .await
6193            .context("reward state did not reach the tip")?;
6194        }
6195        for account in accounts {
6196            assert_same_response(
6197                api_client,
6198                follower_client,
6199                &format!("reward-state-v2/reward-claim-input/{tip}/{account}"),
6200            )
6201            .await?;
6202        }
6203        Ok(())
6204    }
6205
6206    async fn assert_validator_only_routes_not_found(
6207        follower_client: &Client<ClientErr, SequencerApiVersion>,
6208        height: u64,
6209    ) {
6210        for path in [
6211            format!("state-signature/block/{height}"),
6212            "status/keys".to_string(),
6213        ] {
6214            let err = follower_client
6215                .get::<serde_json::Value>(&path)
6216                .send()
6217                .await
6218                .unwrap_err();
6219            assert_matches!(err, ClientErr { status, .. } if status == StatusCode::NOT_FOUND, "{path}");
6220        }
6221    }
6222
6223    /// A path both nodes fail on proves nothing about the follower, so node 0 has to succeed.
6224    async fn assert_same_response(
6225        expected: &Client<ClientErr, SequencerApiVersion>,
6226        actual: &Client<ClientErr, SequencerApiVersion>,
6227        path: &str,
6228    ) -> anyhow::Result<()> {
6229        let expected: serde_json::Value = expected
6230            .get(path)
6231            .send()
6232            .await
6233            .with_context(|| format!("{path}: node 0 did not answer"))?;
6234        let actual = actual
6235            .get::<serde_json::Value>(path)
6236            .send()
6237            .await
6238            .map_err(|err| err.status);
6239        ensure!(
6240            actual.as_ref() == Ok(&expected),
6241            "{path}: follower answered {actual:?}, node 0 answered {expected:?}"
6242        );
6243        Ok(())
6244    }
6245
6246    /// A query node whose database is wiped has to rebuild itself from its peer
6247    /// and rejoin consensus.
6248    ///
6249    /// Six nodes run the new protocol from genesis. Nodes 0 and 1 both serve the
6250    /// query API and are peered at each other so either can backfill from the
6251    /// other; nodes 2 through 5 are plain validators. Once the network decides
6252    /// past epoch seven, node 1 is taken down and started again on an empty
6253    /// database: no consensus state, no archive, no merklized state. To recover
6254    /// it has to bootstrap its stake-table window, resync consensus, backfill
6255    /// the chain it lost from node 0 (including the new protocol's cert2
6256    /// finality certificates), and resume proposing.
6257    #[apply(sql_data_sources)]
6258    async fn test_new_protocol_query_node_restart_with_fresh_storage<D: TestableSqlDataSource>(
6259        _d: PhantomData<D>,
6260    ) -> anyhow::Result<()> {
6261        const NUM_NODES: usize = 6;
6262        const EPOCH_HEIGHT: u64 = 10;
6263        const EPOCHS_BEFORE_RESTART: u64 = 7;
6264        const TEST_UPGRADE: Upgrade = Upgrade::trivial(NEW_PROTOCOL_VERSION);
6265        /// Bound on each stage of the restarted node's recovery.
6266        const RECOVERY_TIMEOUT: Duration = Duration::from_secs(240);
6267
6268        let network_config = TestConfigBuilder::default()
6269            .epoch_height(EPOCH_HEIGHT)
6270            .epoch_start_block(0)
6271            .build();
6272
6273        let api_port = reserve_tcp_port().expect("No ports free for query service");
6274        let query_port = reserve_tcp_port().expect("No ports free for query service");
6275        let api_url: Url = format!("http://localhost:{api_port}").parse()?;
6276        let query_url: Url = format!("http://localhost:{query_port}").parse()?;
6277
6278        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
6279
6280        // Both query nodes need the catchup and light client modules: state
6281        // catchup is served over the former, and the query service validates
6282        // leaves fetched from a peer with light client proofs served over the
6283        // latter. Every node can catch up from either query node.
6284        let query_urls = vec![api_url.clone(), query_url.clone()];
6285        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
6286            .api_config(
6287                Options::with_port(api_port)
6288                    .catchup(Default::default())
6289                    .query_sql(
6290                        Query {
6291                            peers: vec![query_url.clone()],
6292                            ..Default::default()
6293                        },
6294                        D::tmp_options(&storage[0]),
6295                    ),
6296            )
6297            .network_config(network_config)
6298            .persistences(persistence.clone())
6299            .catchups(std::array::from_fn(|_| {
6300                StatePeers::<SequencerApiVersion>::from_urls(
6301                    query_urls.clone(),
6302                    Default::default(),
6303                    Duration::from_secs(2),
6304                    &NoMetrics,
6305                )
6306            }))
6307            .build();
6308        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
6309        let genesis_state = network.genesis_state(0);
6310
6311        // `TestNetwork` only gives node 0 an API, so node 1 has to be served
6312        // separately to become the second query node. Its keys are already in the
6313        // config, so it rejoins under the same index.
6314        network.peers[0].shut_down().await;
6315        network.peers.remove(0);
6316
6317        let cfg = network.cfg.clone();
6318        let peer_url = api_url.clone();
6319        let start_query_node = move |db: persistence::sql::Options| {
6320            serve_query_node(
6321                cfg.clone(),
6322                genesis_state.clone(),
6323                D::sql_persistence_options(db.clone()),
6324                db,
6325                Options::with_port(query_port).catchup(Default::default()),
6326                vec![peer_url.clone()],
6327                peer_url.clone(),
6328            )
6329        };
6330
6331        let mut query_node = start_query_node(D::tmp_options(&storage[1])).await;
6332
6333        let api_client: Client<ClientErr, SequencerApiVersion> = Client::new(api_url);
6334        let query_client: Client<ClientErr, SequencerApiVersion> = Client::new(query_url);
6335        assert!(
6336            api_client.connect(Some(Duration::from_secs(60))).await,
6337            "node 0 query API did not come up"
6338        );
6339        assert!(
6340            query_client.connect(Some(Duration::from_secs(60))).await,
6341            "node 1 query API did not come up"
6342        );
6343
6344        // Run the network for seven epochs, watching a validator that stays up
6345        // for the whole test.
6346        let mut events = network.peers[0].event_stream();
6347        wait_for_epochs(&mut events, EPOCH_HEIGHT, EPOCHS_BEFORE_RESTART).await;
6348
6349        // The node about to be wiped must be healthy first, otherwise the test
6350        // would prove nothing about recovering from a wipe.
6351        let height_before_restart = query_node.decided_leaf().await.height();
6352        timeout(
6353            RECOVERY_TIMEOUT,
6354            wait_until_block_height(
6355                &query_client,
6356                "block-state/block-height",
6357                height_before_restart,
6358            ),
6359        )
6360        .await
6361        .context("second query node was not caught up before the restart")?;
6362
6363        // Pick a height the new protocol finalized with a cert2 to check the
6364        // restarted node backfills those too. A cert2 is only stored at the
6365        // height it finalizes, so scan back from the tip for one.
6366        let mut finalized_height = None;
6367        for height in (1..height_before_restart).rev() {
6368            if api_client
6369                .get::<espresso_types::Certificate2<SeqTypes>>(&format!(
6370                    "availability/cert2/{height}"
6371                ))
6372                .send()
6373                .await
6374                .is_ok()
6375            {
6376                finalized_height = Some(height);
6377                break;
6378            }
6379        }
6380        let finalized_height =
6381            finalized_height.context("no cert2 stored on a new protocol chain")?;
6382
6383        tracing::info!(
6384            height_before_restart,
6385            finalized_height,
6386            "restarting the second query node with fresh storage"
6387        );
6388        query_node.shut_down().await;
6389        // The node has to come back on the same ports: node 0 dials the query
6390        // API at the peer URL baked in at network construction, and cliquenet
6391        // peers dial the address registered in the stake table. `shut_down`
6392        // aborts the server tasks without waiting for them to finish, so poll
6393        // until the old listeners are actually gone before rebinding.
6394        let cliquenet_port = network.cfg.known_nodes_with_stake()[1]
6395            .connect_info
6396            .as_ref()
6397            .expect("node 1 registered cliquenet connect info")
6398            .p2p_addr
6399            .port();
6400        timeout(RECOVERY_TIMEOUT, async {
6401            for port in [query_port, cliquenet_port] {
6402                while std::net::TcpListener::bind(("127.0.0.1", port)).is_err() {
6403                    sleep(Duration::from_millis(100)).await;
6404                }
6405            }
6406        })
6407        .await
6408        .context("shut-down node did not release its ports")?;
6409
6410        let fresh_storage = D::create_storage().await;
6411        let query_node = start_query_node(D::tmp_options(&fresh_storage)).await;
6412        assert!(
6413            query_client.connect(Some(Duration::from_secs(60))).await,
6414            "restarted query API did not come up"
6415        );
6416
6417        // First it has to get back to the height it lost, in both the archive and
6418        // the merklized state derived from it.
6419        timeout(
6420            RECOVERY_TIMEOUT,
6421            wait_until_block_height(&query_client, "status/block-height", height_before_restart),
6422        )
6423        .await
6424        .context("restarted node did not rebuild its archive")?;
6425        timeout(
6426            RECOVERY_TIMEOUT,
6427            wait_until_block_height(
6428                &query_client,
6429                "block-state/block-height",
6430                height_before_restart,
6431            ),
6432        )
6433        .await
6434        .context("restarted node did not rebuild its merklized state")?;
6435
6436        // Reaching the tip only proves it is following consensus again, so stream
6437        // the whole range it lost from both nodes: the restarted node's copy can
6438        // only have come from node 0, and it has to be the chain the network
6439        // actually decided. Streaming also forces the restarted node to backfill
6440        // every leaf in the range, since the stream endpoint fetches on demand.
6441        let wiped_range = (height_before_restart - 1) as usize;
6442        let stream_leaves = |client: Client<ClientErr, SequencerApiVersion>, who: &'static str| async move {
6443            let leaves: Vec<LeafQueryData<SeqTypes>> = client
6444                .socket("availability/stream/leaves/1")
6445                .subscribe()
6446                .await
6447                .with_context(|| format!("subscribing to {who}'s leaf stream"))?
6448                .take(wiped_range)
6449                .try_collect()
6450                .await
6451                .with_context(|| format!("{who}'s leaf stream errored"))?;
6452            anyhow::Ok(leaves)
6453        };
6454        // Node 0's stream returns immediately, so the shared timeout is in
6455        // practice a bound on the restarted node's backfill.
6456        let (ours, theirs) = timeout(
6457            RECOVERY_TIMEOUT,
6458            future::try_join(
6459                stream_leaves(query_client.clone(), "the restarted node"),
6460                stream_leaves(api_client.clone(), "node 0"),
6461            ),
6462        )
6463        .await
6464        .context("streaming the wiped range stalled")??;
6465        assert_eq!(
6466            ours.len(),
6467            wiped_range,
6468            "restarted node's leaf stream ended early"
6469        );
6470        assert_eq!(
6471            theirs.len(),
6472            wiped_range,
6473            "node 0's leaf stream ended early"
6474        );
6475        for (ours, theirs) in ours.iter().zip(&theirs) {
6476            assert_eq!(
6477                ours.hash(),
6478                theirs.hash(),
6479                "restarted node's leaf at height {} diverges from node 0",
6480                ours.height(),
6481            );
6482            assert_eq!(
6483                ours.header().version(),
6484                NEW_PROTOCOL_VERSION,
6485                "block {} should have been produced under the new protocol",
6486                ours.height(),
6487            );
6488        }
6489
6490        let cert2 = timeout(RECOVERY_TIMEOUT, async {
6491            loop {
6492                match query_client
6493                    .get::<espresso_types::Certificate2<SeqTypes>>(&format!(
6494                        "availability/cert2/{finalized_height}"
6495                    ))
6496                    .send()
6497                    .await
6498                {
6499                    Ok(cert2) => break cert2,
6500                    Err(err) => {
6501                        tracing::info!(finalized_height, %err, "cert2 not backfilled yet")
6502                    },
6503                }
6504                sleep(Duration::from_secs(2)).await;
6505            }
6506        })
6507        .await
6508        .context("restarted node did not backfill the cert2")?;
6509        assert_eq!(cert2.data.block_number, finalized_height);
6510
6511        // Tracking decides is not enough to call the node a participant: wait for
6512        // a block it proposed itself to be decided after the restart.
6513        let node_1_key = network.cfg.known_nodes_with_stake()[1]
6514            .stake_table_entry
6515            .stake_key;
6516        let coordinator = query_node.node_state().coordinator;
6517        let mut events = query_node.event_stream();
6518        let proposed = timeout(RECOVERY_TIMEOUT, async {
6519            while let Some(event) = events.next().await {
6520                let leaf_infos: &[LeafInfo<SeqTypes>] = match &event {
6521                    CoordinatorEvent::NewDecide { leaf_infos, .. } => leaf_infos,
6522                    _ => continue,
6523                };
6524                for LeafInfo { leaf, .. } in leaf_infos {
6525                    if leaf.height() <= height_before_restart {
6526                        continue;
6527                    }
6528                    let membership =
6529                        match coordinator.membership_for_epoch(leaf.epoch(EPOCH_HEIGHT)) {
6530                            Ok(membership) => membership,
6531                            Err(err) => {
6532                                tracing::warn!(
6533                                    height = leaf.height(),
6534                                    %err,
6535                                    "no membership for epoch",
6536                                );
6537                                continue;
6538                            },
6539                        };
6540                    match membership.leader(leaf.view_number()) {
6541                        Ok(leader) if leader == node_1_key => return Some(leaf.height()),
6542                        Ok(_) => {},
6543                        Err(err) => {
6544                            tracing::warn!(view = ?leaf.view_number(), %err, "leader unresolved");
6545                        },
6546                    }
6547                }
6548            }
6549            None
6550        })
6551        .await
6552        .context("restarted node's event stream stalled")?;
6553        let proposed = proposed.context("restarted node's event stream ended")?;
6554        tracing::info!(proposed, "restarted node proposed a decided block");
6555
6556        Ok(())
6557    }
6558
6559    #[apply(sql_data_sources)]
6560    async fn test_epoch_reward_total_distributed_rewards<D: TestableSqlDataSource>(
6561        _d: PhantomData<D>,
6562    ) -> anyhow::Result<()> {
6563        // Epochs 1-3: No rewards distributed (total_reward_distributed = 0)
6564        // Epoch 4: Rewards only distributed in the LAST block
6565        // Epoch 5: All blocks before last have same total as epoch 4 last block,
6566        //          last block has higher total because of new distribution
6567        const EPOCH_HEIGHT: u64 = 10;
6568        const NUM_NODES: usize = 5;
6569
6570        let network_config = TestConfigBuilder::default()
6571            .epoch_height(EPOCH_HEIGHT)
6572            .build();
6573
6574        let api_port = reserve_tcp_port().expect("No ports free for query service");
6575
6576        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
6577
6578        let config = TestNetworkConfigBuilder::with_num_nodes()
6579            .api_config(D::options(&storage[0], Options::with_port(api_port)))
6580            .network_config(network_config)
6581            .persistences(persistence.clone())
6582            .build();
6583
6584        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
6585        let client: Client<ClientErr, SequencerApiVersion> =
6586            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
6587
6588        let height_client: Client<ClientErr, StaticVersion<0, 1>> =
6589            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
6590        wait_until_block_height(&height_client, "node/block-height", EPOCH_HEIGHT * 5).await;
6591
6592        let mut leaves = client
6593            .socket("availability/stream/leaves/0")
6594            .subscribe::<LeafQueryData<SeqTypes>>()
6595            .await
6596            .unwrap();
6597
6598        while let Some(leaf) = leaves.next().await {
6599            let leaf = leaf.unwrap();
6600            let header = leaf.header();
6601            let height = header.height();
6602
6603            let total_distributed = header.total_reward_distributed().unwrap();
6604            assert_eq!(total_distributed.0, U256::ZERO,);
6605
6606            if height == EPOCH_HEIGHT * 3 {
6607                break;
6608            }
6609        }
6610
6611        while let Some(leaf) = leaves.next().await {
6612            let leaf = leaf.unwrap();
6613            let header = leaf.header();
6614            let height = header.height();
6615
6616            let total_distributed = header.total_reward_distributed().unwrap();
6617
6618            if height < EPOCH_HEIGHT * 4 {
6619                assert_eq!(total_distributed.0, U256::ZERO,);
6620            } else {
6621                assert!(total_distributed.0 > U256::ZERO,);
6622                break;
6623            }
6624        }
6625
6626        let epoch4_last_reward = {
6627            let header = client
6628                .get::<Header>(&format!("availability/header/{}", EPOCH_HEIGHT * 4))
6629                .send()
6630                .await
6631                .unwrap();
6632            header.total_reward_distributed().unwrap()
6633        };
6634
6635        assert!(
6636            epoch4_last_reward.0 > U256::ZERO,
6637            "epoch 4 last block should have positive rewards"
6638        );
6639
6640        while let Some(leaf) = leaves.next().await {
6641            let leaf = leaf.unwrap();
6642            let header = leaf.header();
6643            let height = header.height();
6644
6645            let total_distributed = header.total_reward_distributed().unwrap();
6646
6647            if height < EPOCH_HEIGHT * 5 {
6648                assert_eq!(total_distributed, epoch4_last_reward,);
6649            } else {
6650                assert!(total_distributed.0 > epoch4_last_reward.0,);
6651                break;
6652            }
6653        }
6654
6655        Ok(())
6656    }
6657
6658    // test actual rewards
6659    // todo: test each account rewards by querying merklized state api
6660    #[apply(sql_data_sources)]
6661    async fn test_reward_state_v2_epoch_distribution<D: TestableSqlDataSource>(
6662        _d: PhantomData<D>,
6663    ) -> anyhow::Result<()> {
6664        const EPOCH_HEIGHT: u64 = 10;
6665        const NUM_NODES: usize = 5;
6666        const NUM_EPOCHS: u64 = 6;
6667
6668        let network_config = TestConfigBuilder::default()
6669            .epoch_height(EPOCH_HEIGHT)
6670            .build();
6671
6672        let api_port = reserve_tcp_port().expect("No ports free for query service");
6673
6674        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
6675
6676        let config = TestNetworkConfigBuilder::with_num_nodes()
6677            .api_config(D::options(&storage[0], Options::with_port(api_port)))
6678            .network_config(network_config)
6679            .persistences(persistence.clone())
6680            .build();
6681
6682        let network = TestNetwork::new(config, TEST_UPGRADE).await;
6683        let client: Client<ClientErr, SequencerApiVersion> =
6684            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
6685
6686        let coordinator = network.server.node_state().coordinator;
6687        assert_epoch_reward_totals(
6688            &client,
6689            &coordinator,
6690            EPOCH_HEIGHT,
6691            NUM_EPOCHS * EPOCH_HEIGHT,
6692        )
6693        .await;
6694
6695        Ok(())
6696    }
6697
6698    /// Verifies that the `leader_counts` array in V5+ headers is correct.
6699    #[apply(sql_data_sources)]
6700    async fn test_epoch_leader_counts<D: TestableSqlDataSource>(
6701        _d: PhantomData<D>,
6702    ) -> anyhow::Result<()> {
6703        const EPOCH_HEIGHT: u64 = 10;
6704        const NUM_NODES: usize = 5;
6705        const NUM_EPOCHS: u64 = 6;
6706
6707        let network_config = TestConfigBuilder::default()
6708            .epoch_height(EPOCH_HEIGHT)
6709            .build();
6710
6711        let api_port = reserve_tcp_port().expect("No ports free for query service");
6712
6713        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
6714
6715        let config = TestNetworkConfigBuilder::with_num_nodes()
6716            .api_config(D::options(&storage[0], Options::with_port(api_port)))
6717            .network_config(network_config)
6718            .persistences(persistence.clone())
6719            .build();
6720
6721        let network = TestNetwork::new(config, TEST_UPGRADE).await;
6722        let client: Client<ClientErr, SequencerApiVersion> =
6723            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
6724
6725        let coordinator = network.server.node_state().coordinator;
6726        assert_epoch_leader_counts(
6727            &client,
6728            &coordinator,
6729            EPOCH_HEIGHT,
6730            NUM_EPOCHS * EPOCH_HEIGHT,
6731        )
6732        .await;
6733
6734        Ok(())
6735    }
6736
6737    /// Mainnet's 0.5 blocks were produced by legacy consensus, so a replayed 0.5 chain is what
6738    /// keeps checking their reward totals and leader counts.
6739    #[apply(sql_data_sources)]
6740    async fn test_legacy_epoch_rewards_and_leader_counts<D: TestableSqlDataSource>(
6741        _d: PhantomData<D>,
6742    ) -> anyhow::Result<()> {
6743        let chain = LegacyChain::load("v5")?;
6744        let replay = chain.replay::<D>().await?;
6745        let epoch_height = chain.epoch_height();
6746        let coordinator = replay.node.node_state().coordinator;
6747
6748        assert_epoch_reward_totals(&replay.client, &coordinator, epoch_height, chain.tip()).await;
6749        assert_epoch_leader_counts(&replay.client, &coordinator, epoch_height, chain.tip()).await;
6750        Ok(())
6751    }
6752
6753    /// Checks `total_reward_distributed` in every header up to `last` against the epoch block
6754    /// rewards.
6755    async fn assert_epoch_reward_totals(
6756        client: &Client<ClientErr, SequencerApiVersion>,
6757        coordinator: &EpochMembershipCoordinator<SeqTypes>,
6758        epoch_height: u64,
6759        last: u64,
6760    ) {
6761        let mut expected_total_distributed = U256::ZERO;
6762        let mut leaves = client
6763            .socket("availability/stream/leaves/0")
6764            .subscribe::<LeafQueryData<SeqTypes>>()
6765            .await
6766            .unwrap();
6767
6768        while let Some(leaf) = leaves.next().await {
6769            let leaf = leaf.unwrap();
6770            let header = leaf.header();
6771            let height = header.height();
6772            let epoch = epoch_from_block_number(height, epoch_height);
6773            let header_total_distributed = header
6774                .total_reward_distributed()
6775                .expect("total_reward_distributed should exist");
6776
6777            if epoch > 3 && height % epoch_height == 0 {
6778                let prev_block_reward = coordinator
6779                    .membership()
6780                    .epoch_block_reward(EpochNumber::new(epoch - 1))
6781                    .expect("epoch block reward should exist");
6782                expected_total_distributed += prev_block_reward.0 * U256::from(epoch_height);
6783            }
6784
6785            assert_eq!(
6786                header_total_distributed.0, expected_total_distributed,
6787                "total_reward_distributed mismatch at height {height}"
6788            );
6789
6790            if height >= last {
6791                break;
6792            }
6793        }
6794    }
6795
6796    /// Checks each header's `leader_counts` up to `last` against the leaders of the epoch's blocks
6797    /// so far.
6798    async fn assert_epoch_leader_counts(
6799        client: &Client<ClientErr, SequencerApiVersion>,
6800        coordinator: &EpochMembershipCoordinator<SeqTypes>,
6801        epoch_height: u64,
6802        last: u64,
6803    ) {
6804        let mut expected_counts: HashMap<Address, u16> = HashMap::new();
6805        let mut leaves = client
6806            .socket("availability/stream/leaves/0")
6807            .subscribe::<LeafQueryData<SeqTypes>>()
6808            .await
6809            .unwrap();
6810
6811        while let Some(leaf) = leaves.next().await {
6812            let leaf = leaf.unwrap();
6813            let header = leaf.header();
6814            let height = header.height();
6815            let epoch = epoch_from_block_number(height, epoch_height);
6816            if epoch <= 2 {
6817                continue;
6818            }
6819
6820            let header_leader_counts = header
6821                .leader_counts()
6822                .expect("V5+ header must have leader_counts");
6823            if (height - 1) % epoch_height == 0 {
6824                expected_counts.clear();
6825            }
6826
6827            let snapshot = coordinator
6828                .membership()
6829                .snapshot(EpochNumber::new(epoch))
6830                .expect("committee for epoch_number");
6831            let leader = snapshot
6832                .leader(leaf.leaf().view_number())
6833                .expect("leader should exist");
6834            let leader_address = snapshot
6835                .validator_config(&leader)
6836                .expect("leader should have an address")
6837                .account;
6838            *expected_counts.entry(leader_address).or_insert(0) += 1;
6839
6840            let header_counts: HashMap<Address, u16> =
6841                ValidatorLeaderCounts::new(&snapshot, *header_leader_counts)
6842                    .expect("ValidatorLeaderCounts should build from header leader_counts")
6843                    .active_leaders()
6844                    .map(|(v, count)| (v.account, count))
6845                    .collect();
6846            assert_eq!(
6847                header_counts, expected_counts,
6848                "leader_counts mismatch at height {height} (epoch {epoch})"
6849            );
6850
6851            if height % epoch_height == 0 {
6852                let total: u16 = expected_counts.values().sum();
6853                assert_eq!(
6854                    total, epoch_height as u16,
6855                    "total leader_counts at epoch boundary should equal the epoch height at \
6856                     height {height}"
6857                );
6858            }
6859
6860            if height >= last {
6861                break;
6862            }
6863        }
6864    }
6865
6866    #[apply(sql_data_sources)]
6867    async fn test_node_stake_table_api<D: TestableSqlDataSource>(_d: PhantomData<D>) {
6868        let epoch_height = 20;
6869
6870        let network_config = TestConfigBuilder::default()
6871            .epoch_height(epoch_height)
6872            .build();
6873
6874        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
6875
6876        const NUM_NODES: usize = 2;
6877        // Initialize nodes.
6878        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
6879
6880        let config = TestNetworkConfigBuilder::with_num_nodes()
6881            .api_config(D::options(&storage[0], Options::with_port(api_port)))
6882            .network_config(network_config)
6883            .persistences(persistence.clone())
6884            .build();
6885
6886        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
6887
6888        let client: Client<ClientErr, SequencerApiVersion> =
6889            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
6890
6891        // wait for atleast 2 epochs
6892        let _blocks = client
6893            .socket("availability/stream/blocks/0")
6894            .subscribe::<BlockQueryData<SeqTypes>>()
6895            .await
6896            .unwrap()
6897            .take(40)
6898            .try_collect::<Vec<_>>()
6899            .await
6900            .unwrap();
6901
6902        for i in 1..=3 {
6903            let _st = client
6904                .get::<Vec<PeerConfig<SeqTypes>>>(&format!("node/stake-table/{}", i as u64))
6905                .send()
6906                .await
6907                .expect("failed to get stake table");
6908        }
6909
6910        let _st = client
6911            .get::<StakeTableWithEpochNumber<SeqTypes>>("node/stake-table/current")
6912            .send()
6913            .await
6914            .expect("failed to get stake table");
6915    }
6916
6917    #[apply(sql_data_sources)]
6918    async fn test_epoch_stake_table_catchup<D: TestableSqlDataSource>(_d: PhantomData<D>) {
6919        const EPOCH_HEIGHT: u64 = 10;
6920        const NUM_NODES: usize = 6;
6921
6922        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
6923
6924        let network_config = TestConfigBuilder::default()
6925            .epoch_height(EPOCH_HEIGHT)
6926            .build();
6927
6928        // Initialize storage for each node
6929        let (storage, persistence_options) = node_storage::<D, NUM_NODES>().await;
6930
6931        // setup catchup peers
6932        let catchup_peers = std::array::from_fn(|_| {
6933            StatePeers::<StaticVersion<0, 1>>::from_urls(
6934                vec![format!("http://localhost:{port}").parse().unwrap()],
6935                Default::default(),
6936                Duration::from_secs(2),
6937                &NoMetrics,
6938            )
6939        });
6940        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
6941            .api_config(D::options(&storage[0], Options::with_port(port)))
6942            .network_config(network_config)
6943            .persistences(persistence_options.clone())
6944            .catchups(catchup_peers)
6945            .build();
6946        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
6947        let state = network.genesis_state(0);
6948
6949        // Wait for the peer 0 (node 1) to advance past three epochs
6950        let mut events = network.peers[0].event_stream();
6951        while let Some(event) = events.next().await {
6952            if let CoordinatorEvent::NewDecide {
6953                leaf_infos: leaf_chain,
6954                ..
6955            } = event
6956            {
6957                let height = leaf_chain[0].leaf.height();
6958                tracing::info!("Node 0 decided at height: {height}");
6959                if height > EPOCH_HEIGHT * 3 {
6960                    break;
6961                }
6962            }
6963        }
6964
6965        // Shutdown and remove node 1 to simulate falling behind
6966        tracing::info!("Shutting down peer 0");
6967        network.peers.remove(0);
6968
6969        // Wait for epochs to progress with node 1 offline
6970        let mut events = network.server.event_stream();
6971        while let Some(event) = events.next().await {
6972            if let CoordinatorEvent::NewDecide {
6973                leaf_infos: leaf_chain,
6974                ..
6975            } = event
6976            {
6977                let height = leaf_chain[0].leaf.height();
6978                if height > EPOCH_HEIGHT * 7 {
6979                    break;
6980                }
6981            }
6982        }
6983
6984        // add node 1 to the network with fresh storage
6985        let storage = D::create_storage().await;
6986        let options = <D as TestableSequencerDataSource>::persistence_options(&storage);
6987        tracing::info!("Restarting peer 0");
6988        let node = network
6989            .cfg
6990            .init_node(
6991                1,
6992                state,
6993                options,
6994                Some(StatePeers::<StaticVersion<0, 1>>::from_urls(
6995                    vec![format!("http://localhost:{port}").parse().unwrap()],
6996                    Default::default(),
6997                    Duration::from_secs(2),
6998                    &NoMetrics,
6999                )),
7000                None,
7001                &NoMetrics,
7002                test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
7003                NullEventConsumer,
7004                TEST_UPGRADE,
7005                Default::default(),
7006            )
7007            .await;
7008
7009        let coordinator = node.node_state().coordinator;
7010        let server_node_state = network.server.node_state();
7011        let server_coordinator = server_node_state.coordinator;
7012        // Verify that the restarted node catches up for each epoch
7013        for epoch_num in 1..=7 {
7014            let epoch = EpochNumber::new(epoch_num);
7015            let node_em = match coordinator.membership_for_epoch(Some(epoch)) {
7016                Ok(em) => em,
7017                Err(_) => coordinator.wait_for_catchup(epoch).await.unwrap(),
7018            };
7019            let server_em = match server_coordinator.membership_for_epoch(Some(epoch)) {
7020                Ok(em) => em,
7021                Err(_) => server_coordinator.wait_for_catchup(epoch).await.unwrap(),
7022            };
7023
7024            println!("have stake table for epoch = {epoch_num}");
7025
7026            let node_stake_table = HSStakeTable::from_iter(node_em.stake_table());
7027            let stake_table = HSStakeTable::from_iter(server_em.stake_table());
7028            println!("asserting stake table for epoch = {epoch_num}");
7029
7030            assert_eq!(
7031                node_stake_table, stake_table,
7032                "Stake table mismatch for epoch {epoch_num}",
7033            );
7034        }
7035    }
7036
7037    #[apply(sql_data_sources)]
7038    async fn test_epoch_stake_table_catchup_stress<D: TestableSqlDataSource>(_d: PhantomData<D>) {
7039        const EPOCH_HEIGHT: u64 = 10;
7040        const NUM_NODES: usize = 6;
7041
7042        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7043
7044        let network_config = TestConfigBuilder::default()
7045            .epoch_height(EPOCH_HEIGHT)
7046            .build();
7047
7048        // Initialize storage for each node
7049        let (storage, persistence_options) = node_storage::<D, NUM_NODES>().await;
7050
7051        // setup catchup peers
7052        let catchup_peers = std::array::from_fn(|_| {
7053            StatePeers::<StaticVersion<0, 1>>::from_urls(
7054                vec![format!("http://localhost:{port}").parse().unwrap()],
7055                Default::default(),
7056                Duration::from_secs(2),
7057                &NoMetrics,
7058            )
7059        });
7060        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
7061            .api_config(D::options(&storage[0], Options::with_port(port)))
7062            .network_config(network_config)
7063            .persistences(persistence_options.clone())
7064            .catchups(catchup_peers)
7065            .build();
7066        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
7067        let state = network.genesis_state(0);
7068
7069        // Wait for the peer 0 (node 1) to advance past three epochs
7070        let mut events = network.peers[0].event_stream();
7071        while let Some(event) = events.next().await {
7072            if let CoordinatorEvent::NewDecide {
7073                leaf_infos: leaf_chain,
7074                ..
7075            } = event
7076            {
7077                let height = leaf_chain[0].leaf.height();
7078                tracing::info!("Node 0 decided at height: {height}");
7079                if height > EPOCH_HEIGHT * 3 {
7080                    break;
7081                }
7082            }
7083        }
7084
7085        // Shutdown and remove node 1 to simulate falling behind
7086        tracing::info!("Shutting down peer 0");
7087        network.peers.remove(0);
7088
7089        // Wait for epochs to progress with node 1 offline
7090        let mut events = network.server.event_stream();
7091        while let Some(event) = events.next().await {
7092            if let CoordinatorEvent::NewDecide {
7093                leaf_infos: leaf_chain,
7094                ..
7095            } = event
7096            {
7097                let height = leaf_chain[0].leaf.height();
7098                tracing::info!("Server decided at height: {height}");
7099                //  until 7 epochs
7100                if height > EPOCH_HEIGHT * 7 {
7101                    break;
7102                }
7103            }
7104        }
7105
7106        // add node 1 to the network with fresh storage
7107        let storage = D::create_storage().await;
7108        let options = <D as TestableSequencerDataSource>::persistence_options(&storage);
7109
7110        tracing::info!("Restarting peer 0");
7111        let node = network
7112            .cfg
7113            .init_node(
7114                1,
7115                state,
7116                options,
7117                Some(StatePeers::<StaticVersion<0, 1>>::from_urls(
7118                    vec![format!("http://localhost:{port}").parse().unwrap()],
7119                    Default::default(),
7120                    Duration::from_secs(2),
7121                    &NoMetrics,
7122                )),
7123                None,
7124                &NoMetrics,
7125                test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
7126                NullEventConsumer,
7127                TEST_UPGRADE,
7128                Default::default(),
7129            )
7130            .await;
7131
7132        let coordinator = node.node_state().coordinator;
7133
7134        let server_node_state = network.server.node_state();
7135        let server_coordinator = server_node_state.coordinator;
7136
7137        // Trigger catchup for all epochs in quick succession and in random order
7138        let mut rand_epochs: Vec<_> = (1..=7).collect();
7139        rand_epochs.shuffle(&mut rand::thread_rng());
7140        println!("trigger catchup in this order: {rand_epochs:?}");
7141        for epoch_num in rand_epochs {
7142            let epoch = EpochNumber::new(epoch_num);
7143            let _ = coordinator.membership_for_epoch(Some(epoch));
7144        }
7145
7146        // Verify that the restarted node catches up for each epoch
7147        for epoch_num in 1..=7 {
7148            println!("getting stake table for epoch = {epoch_num}");
7149            let epoch = EpochNumber::new(epoch_num);
7150            let node_em = coordinator.wait_for_catchup(epoch).await.unwrap();
7151            let server_em = match server_coordinator.membership_for_epoch(Some(epoch)) {
7152                Ok(em) => em,
7153                Err(_) => server_coordinator.wait_for_catchup(epoch).await.unwrap(),
7154            };
7155
7156            println!("have stake table for epoch = {epoch_num}");
7157
7158            let node_stake_table = HSStakeTable::from_iter(node_em.stake_table());
7159            let stake_table = HSStakeTable::from_iter(server_em.stake_table());
7160
7161            println!("asserting stake table for epoch = {epoch_num}");
7162
7163            assert_eq!(
7164                node_stake_table, stake_table,
7165                "Stake table mismatch for epoch {epoch_num}",
7166            );
7167        }
7168    }
7169
7170    #[apply(sql_data_sources)]
7171    async fn test_merklized_state_catchup_on_restart<D: TestableSqlDataSource>(
7172        _d: PhantomData<D>,
7173    ) -> anyhow::Result<()> {
7174        // This test verifies that a query node can catch up on
7175        // merklized state after being offline for multiple epochs.
7176        //
7177        // Steps:
7178        // 1. Start a test network with 5 sequencer nodes.
7179        // 2. Start a separate node with the query module enabled, connected to the network.
7180        //    - This node stores merklized state
7181        // 3. Shut down the query node after 1 epoch.
7182        // 4. Allow the network to progress 3 more epochs (query node remains offline).
7183        // 5. Restart the query node.
7184        //    - The node is expected to reconstruct or catch up on its own
7185        use espresso_types::{DECAF_CHAIN_ID, v0_3::ChainConfig};
7186
7187        const EPOCH_HEIGHT: u64 = 10;
7188
7189        let network_config = TestConfigBuilder::default()
7190            .epoch_height(EPOCH_HEIGHT)
7191            .build();
7192
7193        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7194
7195        tracing::info!("API PORT = {api_port}");
7196        const NUM_NODES: usize = 5;
7197
7198        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
7199
7200        // The light client skips epoch-root stake-table-hash verification for pre-DRB headers only
7201        // on the Decaf chain id, so use it to keep the V3->V4 catchup path covered. The stake
7202        // table deploy in `TestNetwork::new` keeps the chain id from `state[0]`.
7203        let decaf_state = ValidatedState {
7204            chain_config: ChainConfig {
7205                chain_id: DECAF_CHAIN_ID,
7206                ..Default::default()
7207            }
7208            .into(),
7209            ..Default::default()
7210        };
7211
7212        let config = TestNetworkConfigBuilder::with_num_nodes()
7213            .api_config(D::options(
7214                &storage[0],
7215                Options::with_port(api_port).catchup(Default::default()),
7216            ))
7217            .network_config(network_config)
7218            .persistences(persistence.clone())
7219            .states(std::array::from_fn(|_| decaf_state.clone()))
7220            .build();
7221        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
7222        let state = network.genesis_state(0);
7223
7224        // Remove peer 0 and restart it with the query module enabled.
7225        // Adding an additional node to the test network is not straight forward,
7226        // as the keys have already been initialized in the config above.
7227        // So, we remove this node and re-add it using the same index.
7228        network.peers[0].shut_down().await;
7229        network.peers.remove(0);
7230        let node_0_storage = &storage[1];
7231        let node_0_persistence = persistence[1].clone();
7232        let node_0_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7233        tracing::info!("node_0_port {node_0_port}");
7234        // enable query module with api peers
7235        let opt = Options::with_port(node_0_port).query_sql(
7236            Query {
7237                peers: vec![format!("http://localhost:{api_port}").parse().unwrap()],
7238                ..Default::default()
7239            },
7240            D::tmp_options(node_0_storage),
7241        );
7242
7243        // start the query node so that it builds the merklized state
7244        let mut node_0 = opt
7245            .clone()
7246            .serve(|metrics, consumer, storage| {
7247                let cfg = network.cfg.clone();
7248                let node_0_persistence = node_0_persistence.clone();
7249                let state = state.clone();
7250                async move {
7251                    Ok(cfg
7252                        .init_node(
7253                            1,
7254                            state,
7255                            node_0_persistence.clone(),
7256                            Some(StatePeers::<StaticVersion<0, 1>>::from_urls(
7257                                vec![format!("http://localhost:{api_port}").parse().unwrap()],
7258                                Default::default(),
7259                                Duration::from_secs(2),
7260                                &NoMetrics,
7261                            )),
7262                            storage,
7263                            &*metrics,
7264                            test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
7265                            consumer,
7266                            TEST_UPGRADE,
7267                            Default::default(),
7268                        )
7269                        .await)
7270                }
7271                .boxed()
7272            })
7273            .await
7274            .unwrap();
7275        node_0.start_consensus().await;
7276
7277        let mut events = network.peers[2].event_stream();
7278        // wait for 1 epoch
7279        wait_for_epochs(&mut events, EPOCH_HEIGHT, 1).await;
7280
7281        // Shut the node down for 3 epochs. Await it so its cliquenet listener releases its port
7282        // before the restarted node binds the same address.
7283        node_0.shut_down().await;
7284
7285        // wait for 4 epochs
7286        wait_for_epochs(&mut events, EPOCH_HEIGHT, 4).await;
7287
7288        // start the node again.
7289        tracing::info!("restarting node");
7290        let node_0 = opt
7291            .serve(|metrics, consumer, storage| {
7292                let cfg = network.cfg.clone();
7293                async move {
7294                    Ok(cfg
7295                        .init_node(
7296                            1,
7297                            state,
7298                            node_0_persistence,
7299                            Some(StatePeers::<StaticVersion<0, 1>>::from_urls(
7300                                vec![format!("http://localhost:{api_port}").parse().unwrap()],
7301                                Default::default(),
7302                                Duration::from_secs(2),
7303                                &NoMetrics,
7304                            )),
7305                            storage,
7306                            &*metrics,
7307                            test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
7308                            consumer,
7309                            TEST_UPGRADE,
7310                            Default::default(),
7311                        )
7312                        .await)
7313                }
7314                .boxed()
7315            })
7316            .await
7317            .unwrap();
7318        node_0.start_consensus().await;
7319
7320        let client: Client<ClientErr, SequencerApiVersion> =
7321            Client::new(format!("http://localhost:{node_0_port}").parse().unwrap());
7322        client.connect(None).await;
7323
7324        wait_for_epochs(&mut events, EPOCH_HEIGHT, 6).await;
7325
7326        let epoch_7_block = EPOCH_HEIGHT * 6 + 1;
7327
7328        // check that the node's state has reward accounts
7329        let mut retries = 0;
7330        loop {
7331            sleep(Duration::from_secs(1)).await;
7332            let leaves = node_0
7333                .decided_state()
7334                .await
7335                .map_or(0, |state| state.reward_merkle_tree_v2.num_leaves());
7336
7337            if leaves > 0 {
7338                tracing::info!("Node's state has reward accounts");
7339                break;
7340            }
7341
7342            retries += 1;
7343            if retries > 120 {
7344                panic!("max retries reached. failed to catchup reward state");
7345            }
7346        }
7347
7348        retries = 0;
7349        // check that the node has stored atleast 6 epochs merklized state in persistence
7350        loop {
7351            sleep(Duration::from_secs(3)).await;
7352
7353            let bh = client
7354                .get::<u64>("block-state/block-height")
7355                .send()
7356                .await
7357                .expect("block height not found");
7358
7359            tracing::info!("block state: block height={bh}");
7360            if bh > epoch_7_block {
7361                break;
7362            }
7363
7364            retries += 1;
7365            if retries > 30 {
7366                panic!(
7367                    "max retries reached. block state block height is less than epoch 7 start \
7368                     block"
7369                );
7370            }
7371        }
7372
7373        let decided_leaf = node_0.decided_leaf().await;
7374        let state = node_0.decided_state().await.unwrap();
7375        node_0.shutdown_consensus().await;
7376        tracing::info!(
7377            height = decided_leaf.height(),
7378            ?decided_leaf,
7379            ?state,
7380            "final state"
7381        );
7382
7383        let height = decided_leaf.height();
7384        let num_leaves = state.block_merkle_tree.num_leaves();
7385        tracing::info!(height, num_leaves, "checking block merkle tree state");
7386        state
7387            .block_merkle_tree
7388            .lookup(height - 1)
7389            .expect_ok()
7390            .unwrap_or_else(|err| {
7391                panic!(
7392                    "block state not found ({err:#}):\n{:#?}",
7393                    state.block_merkle_tree
7394                )
7395            });
7396
7397        Ok(())
7398    }
7399
7400    /// A node that was down for longer than its state retention comes back with its merklized
7401    /// state well behind the chain. The state loop can only continue from its newest snapshot, so
7402    /// the state pruner must not get ahead of it. Restart a query node behind a pruned height that
7403    /// an unbounded pruner run has already stamped past its state head, and check that the loop
7404    /// still resumes and reaches the tip, and that every pruner run from then on stamps below the
7405    /// state head.
7406    #[apply(sql_data_sources)]
7407    async fn test_state_pruner_stays_below_state_head<D: TestableSqlDataSource>(
7408        _d: PhantomData<D>,
7409    ) -> anyhow::Result<()> {
7410        const NUM_NODES: usize = 5;
7411        // Blocks the chain moves on by while the query node is down, and the margin by which the
7412        // loop has to get past the stale pruned height before the check ends.
7413        const GAP: u64 = 10;
7414
7415        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
7416        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7417        let api_url: Url = format!("http://localhost:{api_port}").parse().unwrap();
7418        // The query node fetches what it missed from the API node, so the API node serves catchup
7419        // and light client proofs.
7420        let config = TestNetworkConfigBuilder::with_num_nodes()
7421            .api_config(D::options(
7422                &storage[0],
7423                Options::with_port(api_port).catchup(Default::default()),
7424            ))
7425            .network_config(TestConfigBuilder::default().build())
7426            .persistences(persistence.clone())
7427            .build();
7428        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
7429        let genesis_state = network.genesis_state(0);
7430
7431        // Replace peer 0 with a query node whose state pruner runs every second with zero
7432        // retention: left to itself, every run would prune right up to the chain tip. Consensus
7433        // data stays out of it: the test genesis has timestamp 0, so any data retention would
7434        // delete the genesis leaf the loop's first catchup reads.
7435        network.peers[0].shut_down().await;
7436        network.peers.remove(0);
7437        let query_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7438        let mut db_opt = D::tmp_options(&storage[1]);
7439        db_opt.prune = true;
7440        db_opt.pruning = <persistence::sql::PruningOptions as clap::Parser>::parse_from([
7441            "pruning",
7442            "--target-retention",
7443            "4294967295s",
7444            "--state-target-retention",
7445            "0s",
7446            "--interval",
7447            "1s",
7448        ]);
7449        let start_query_node = {
7450            let cfg = network.cfg.clone();
7451            let node_persistence = persistence[1].clone();
7452            let db_opt = db_opt.clone();
7453            let api_url = api_url.clone();
7454            move || {
7455                serve_query_node(
7456                    cfg.clone(),
7457                    genesis_state.clone(),
7458                    node_persistence.clone(),
7459                    db_opt.clone(),
7460                    Options::with_port(query_port),
7461                    vec![api_url.clone()],
7462                    api_url.clone(),
7463                )
7464            }
7465        };
7466
7467        let mut query_node = start_query_node().await;
7468        let query_client: Client<ClientErr, StaticVersion<0, 1>> =
7469            Client::new(format!("http://localhost:{query_port}").parse().unwrap());
7470        assert!(query_client.connect(Some(Duration::from_secs(60))).await);
7471        let api_client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(api_url.clone());
7472        assert!(api_client.connect(Some(Duration::from_secs(60))).await);
7473
7474        // Let the loop build some state, then take the node down while the chain moves on.
7475        wait_until_block_height(&query_client, "block-state/block-height", 5).await;
7476        tracing::info!("query node has state; shutting it down");
7477        query_node.shut_down().await;
7478        drop(query_node);
7479        // `shut_down` aborts the server task without waiting for it, so wait for the port before
7480        // rebinding it.
7481        timeout(Duration::from_secs(30), async {
7482            while std::net::TcpListener::bind(("127.0.0.1", query_port)).is_err() {
7483                sleep(Duration::from_millis(100)).await;
7484            }
7485        })
7486        .await
7487        .context("shut-down query node did not release its port")?;
7488        let db = D::connect(&db_opt, StorageConnectionType::Query).await?;
7489        let (_, head_at_shutdown) = D::state_heights(&db).await?;
7490        wait_until_block_height(&api_client, "status/block-height", head_at_shutdown + GAP).await;
7491        // `status/block-height` counts blocks, so the tip is one below it.
7492        let stale_cursor = api_client.get::<u64>("status/block-height").send().await? - 1;
7493        tracing::info!(
7494            head_at_shutdown,
7495            stale_cursor,
7496            "chain moved on; restarting query node"
7497        );
7498
7499        // A pruner from before the state pruner was bounded by the state head could have stamped
7500        // past the loop while it was behind. Stamp the tip as such a run would have.
7501        D::stamp_state_pruned_height(&db, stale_cursor).await?;
7502        ensure!(
7503            D::state_heights(&db).await?.0 == Some(stale_cursor),
7504            "the stale cursor was not stamped"
7505        );
7506
7507        // The loop has to resume from its head, get past the stale cursor and reach the tip, and
7508        // every pruner run from then on has to stamp below the head.
7509        let _query_node = start_query_node().await;
7510        let deadline = Instant::now() + Duration::from_secs(120);
7511        loop {
7512            let (pruned, head) = D::state_heights(&db).await?;
7513            let pruned = pruned.context("state pruned height is stamped")?;
7514            tracing::info!(pruned, head, stale_cursor, "state loop progress");
7515            if pruned > stale_cursor {
7516                ensure!(
7517                    pruned < head,
7518                    "state pruner passed the state head: pruned {pruned}, head {head}"
7519                );
7520                if head > stale_cursor + GAP {
7521                    break;
7522                }
7523            }
7524            ensure!(
7525                Instant::now() < deadline,
7526                "state loop did not get past the stale pruned height {stale_cursor}: pruned \
7527                 {pruned}, head {head}"
7528            );
7529            sleep(Duration::from_millis(200)).await;
7530        }
7531
7532        // The head snapshot is served. The loop and the pruner keep moving, so a head read just
7533        // before a pruner run can be below the cursor by the time the query runs; read it again.
7534        for attempt in 1.. {
7535            let (_, head) = D::state_heights(&db).await?;
7536            let frontier = query_client
7537                .get::<MerkleProof<Commitment<Header>, u64, Sha3Node, 3>>(&format!(
7538                    "block-state/{head}/{}",
7539                    head - 1
7540                ))
7541                .send()
7542                .await;
7543            match frontier {
7544                Ok(_) => break,
7545                Err(err) if attempt < 10 => {
7546                    tracing::info!(head, %err, "retrying the head frontier query");
7547                    sleep(Duration::from_millis(200)).await;
7548                },
7549                Err(err) => bail!("block frontier at the state head {head} is not served: {err}"),
7550            }
7551        }
7552        Ok(())
7553    }
7554
7555    /// When the state loop meets the data pruned height, as on a node that was down for longer
7556    /// than its data retention.
7557    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
7558    enum DataPruned {
7559        /// The marker was stamped past the state head while the node was down: the loop finds
7560        /// its head leaf gone and starts on peers.
7561        BeforeRestart,
7562        /// The marker is stamped while the loop waits on the local stream for a leaf that never
7563        /// comes: the loop has to notice and switch to peers. The loop is put well behind first,
7564        /// as a long outage would, since a gap above the consensus anchor is re-decided on
7565        /// restart and cannot be kept open, and the peers stay unreachable until the stamp, since
7566        /// the fetcher would otherwise fill the gap.
7567        WhileWaiting,
7568    }
7569
7570    /// The availability layer never fetches a leaf at or below the data pruned height, so the
7571    /// state loop has to get the leaves it still needs from peers, without storing them, and
7572    /// carry on from the local stream once it is past the pruned height. The leaves come through
7573    /// the light client, verified against per-epoch stake tables, so the network runs with epochs.
7574    #[rstest]
7575    #[case::before_restart(DataPruned::BeforeRestart)]
7576    #[case::while_waiting(DataPruned::WhileWaiting)]
7577    #[test_log::test(tokio::test(flavor = "multi_thread"))]
7578    async fn test_state_loop_fetches_pruned_leaves_from_peers<D: TestableSqlDataSource>(
7579        #[values(PhantomData::<Postgres>, PhantomData::<Sqlite>)] _d: PhantomData<D>,
7580
7581        #[case] pruned_when: DataPruned,
7582    ) -> anyhow::Result<()> {
7583        const NUM_NODES: usize = 5;
7584        const EPOCH_HEIGHT: u64 = 10;
7585        // Leaves the node holds beyond the state head when it goes down, the margin by which the
7586        // loop has to get past the pruned height before the check ends, and the head the loop is
7587        // put back to when it has to be left waiting.
7588        const SYNCED: u64 = 5;
7589        const GAP: u64 = 10;
7590        const BEHIND: u64 = 2;
7591
7592        let storage = join_all((0..NUM_NODES).map(|_| D::create_storage())).await;
7593        let persistence: [_; NUM_NODES] = storage
7594            .iter()
7595            .map(<D as TestableSequencerDataSource>::persistence_options)
7596            .collect::<Vec<_>>()
7597            .try_into()
7598            .unwrap();
7599        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7600        let api_url: Url = format!("http://localhost:{api_port}").parse().unwrap();
7601        // The API node is the peer the query node fetches leaves from.
7602        let config = TestNetworkConfigBuilder::with_num_nodes()
7603            .api_config(D::options(
7604                &storage[0],
7605                Options::with_port(api_port).catchup(Default::default()),
7606            ))
7607            .network_config(
7608                TestConfigBuilder::default()
7609                    .epoch_height(EPOCH_HEIGHT)
7610                    .build(),
7611            )
7612            .persistences(persistence.clone())
7613            .catchups(std::array::from_fn(|_| {
7614                StatePeers::<StaticVersion<0, 1>>::from_urls(
7615                    vec![api_url.clone()],
7616                    Default::default(),
7617                    Duration::from_secs(2),
7618                    &NoMetrics,
7619                )
7620            }))
7621            .pos_hook(
7622                DelegationConfig::MultipleDelegators,
7623                StakeTableContractVersion::V3,
7624                TEST_UPGRADE,
7625            )
7626            .await?
7627            .build();
7628        let genesis_state = config.states()[0].clone();
7629        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
7630
7631        // Replace peer 0 with a query node. Its availability fetcher gets the given peers: the
7632        // API node, except when the loop has to be left waiting for a leaf that never arrives.
7633        network.peers[0].shut_down().await;
7634        network.peers.remove(0);
7635        let query_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7636        let db_opt = D::tmp_options(&storage[1]);
7637        let start_query_node = {
7638            let cfg = network.cfg.clone();
7639            let node_persistence = persistence[1].clone();
7640            let db_opt = db_opt.clone();
7641            let api_url = api_url.clone();
7642            move |availability_peers: Vec<Url>| {
7643                serve_query_node(
7644                    cfg.clone(),
7645                    genesis_state.clone(),
7646                    node_persistence.clone(),
7647                    db_opt.clone(),
7648                    Options::with_port(query_port),
7649                    availability_peers,
7650                    api_url.clone(),
7651                )
7652            }
7653        };
7654
7655        let mut query_node = start_query_node(vec![api_url.clone()]).await;
7656        let query_client: Client<ClientErr, StaticVersion<0, 1>> =
7657            Client::new(format!("http://localhost:{query_port}").parse().unwrap());
7658        assert!(query_client.connect(Some(Duration::from_secs(60))).await);
7659        let api_client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(api_url.clone());
7660        assert!(api_client.connect(Some(Duration::from_secs(60))).await);
7661
7662        // Let the loop build some state, then take the node down while the chain moves on.
7663        wait_until_block_height(&query_client, "block-state/block-height", 5).await;
7664        query_node.shut_down().await;
7665        drop(query_node);
7666        // `shut_down` aborts the server task without waiting for it, so wait for the port before
7667        // rebinding it.
7668        timeout(Duration::from_secs(30), async {
7669            while std::net::TcpListener::bind(("127.0.0.1", query_port)).is_err() {
7670                sleep(Duration::from_millis(100)).await;
7671            }
7672        })
7673        .await
7674        .context("shut-down query node did not release its port")?;
7675        let db = D::connect(&db_opt, StorageConnectionType::Query).await?;
7676        let (_, head_at_shutdown) = D::state_heights(&db).await?;
7677        let local_tip = D::local_block_height(&db).await? - 1;
7678
7679        let _restarted;
7680        let pruned = match pruned_when {
7681            DataPruned::BeforeRestart => {
7682                // Prune consensus data past the state head, as a pruner run during the downtime
7683                // would have once the retention passed. A run never stamps past the node's own
7684                // tip, so first give the node a leaf beyond the marker, as its fetcher would have
7685                // before the outage outran the loop.
7686                let synced_tip = local_tip + SYNCED;
7687                let pruned = synced_tip - 1;
7688                wait_until_block_height(&api_client, "status/block-height", synced_tip + 1).await;
7689                let synced_leaf = api_client
7690                    .get::<LeafQueryData<SeqTypes>>(&format!("availability/leaf/{synced_tip}"))
7691                    .send()
7692                    .await?;
7693                D::insert_leaf(&db, &synced_leaf).await?;
7694                D::prune_data_to(&db, pruned).await?;
7695                _restarted = start_query_node(vec![api_url.clone()]).await;
7696                pruned
7697            },
7698            DataPruned::WhileWaiting => {
7699                // Put the loop behind its own storage and take the leaves above its new head out
7700                // of local reach. Below the consensus anchor they are never re-decided, and with
7701                // the peers unreachable they are never fetched. The loop then waits on the stream
7702                // for the leaf above its head while consensus moves the tip on.
7703                let proxy_port =
7704                    reserve_tcp_port().expect("OS should have ephemeral ports available");
7705                D::set_last_state_height(&db, BEHIND).await?;
7706                D::delete_data_between(&db, BEHIND, head_at_shutdown).await?;
7707                _restarted =
7708                    start_query_node(vec![format!("http://localhost:{proxy_port}").parse()?]).await;
7709                let deadline = Instant::now() + Duration::from_secs(120);
7710                loop {
7711                    if D::local_block_height(&db).await? > local_tip + GAP {
7712                        break;
7713                    }
7714                    ensure!(
7715                        Instant::now() < deadline,
7716                        "consensus did not move the local tip past {}",
7717                        local_tip + GAP
7718                    );
7719                    sleep(Duration::from_millis(200)).await;
7720                }
7721                let (_, head) = D::state_heights(&db).await?;
7722                ensure!(
7723                    head == BEHIND,
7724                    "the loop moved to {head} without the leaf above {BEHIND}"
7725                );
7726                // Now stamp past the height the loop is waiting for, with the old head well below
7727                // the tip as a real run leaves it, and let the peers through. The fetcher no
7728                // longer asks them for heights at or below the stamp, so the loop has to.
7729                let pruned = head_at_shutdown;
7730                D::prune_data_to(&db, pruned).await?;
7731                tokio::spawn(forward_tcp(proxy_port, api_port));
7732                pruned
7733            },
7734        };
7735
7736        // The loop has to get past the pruned height on leaves from the API node, and then follow
7737        // the local stream.
7738        let deadline = Instant::now() + Duration::from_secs(180);
7739        loop {
7740            let (_, head) = D::state_heights(&db).await?;
7741            if head > pruned + GAP {
7742                break;
7743            }
7744            ensure!(
7745                Instant::now() < deadline,
7746                "state loop did not get past the data pruned height {pruned}: head {head}, \
7747                 started from {head_at_shutdown}"
7748            );
7749            sleep(Duration::from_millis(200)).await;
7750        }
7751
7752        // The leaves the loop fetched from peers were not stored: the pruner would only delete
7753        // them again.
7754        let stored = D::count_leaves_up_to(&db, pruned).await?;
7755        ensure!(
7756            stored == 0,
7757            "{stored} leaves at or below the data pruned height {pruned} were stored"
7758        );
7759        Ok(())
7760    }
7761
7762    /// A query node with no state of its own on a pruned database, as a fresh pruned node, starts
7763    /// its state from a snapshot at a recent epoch boundary, from peers, instead of replaying the
7764    /// chain from genesis. The loop gets past the next boundary only if the reward tree it fetched
7765    /// is whole. The snapshot has no fee accounts, and from V0.6 on no block reads one, so the node
7766    /// answers that the fee state is missing from the boundary on, and pruned below it. A
7767    /// brand-new database, which the data pruner has not stamped yet, starts from a boundary too,
7768    /// and marks the blocks below its first block as pruned, so it never fetches them.
7769    /// A node without a pruner, here one that pruned before and has turned pruning off, still
7770    /// replays from genesis, on leaves from peers up to the data pruned height.
7771    #[rstest]
7772    #[case::recent_boundary(EmptyStateStart::RecentBoundary, true)]
7773    #[case::fresh_database(EmptyStateStart::RecentBoundary, false)]
7774    #[case::genesis(EmptyStateStart::Genesis, true)]
7775    #[test_log::test(tokio::test(flavor = "multi_thread"))]
7776    async fn test_state_loop_starts_on_pruned_data<D: TestableSqlDataSource>(
7777        #[values(PhantomData::<Postgres>, PhantomData::<Sqlite>)] _d: PhantomData<D>,
7778        #[case] expected: EmptyStateStart,
7779        #[case] stamp_pruned_height: bool,
7780    ) -> anyhow::Result<()> {
7781        let network = PrunedStartNetwork::<D>::start().await?;
7782        // Let the chain run an epoch past the data pruned height stamped below, so a recent
7783        // boundary lies above it.
7784        let stamp = 3 * PRUNED_START_EPOCH_HEIGHT + PRUNED_START_EPOCH_HEIGHT / 2;
7785        wait_until_block_height(
7786            &network.api_client,
7787            "status/block-height",
7788            stamp + PRUNED_START_EPOCH_HEIGHT,
7789        )
7790        .await;
7791        let (db_opt, db) = network.query_database(expected).await?;
7792        // Unless the database is brand new, stamp a data pruned height on it, as if the data
7793        // pruner had run before the loop started.
7794        let pruned = if stamp_pruned_height {
7795            D::prune_data_to(&db, stamp).await?;
7796            stamp
7797        } else {
7798            0
7799        };
7800        // `status/block-height` counts blocks, so the tip is one below it.
7801        let tip = network
7802            .api_client
7803            .get::<u64>("status/block-height")
7804            .send()
7805            .await?
7806            - 1;
7807        let (_query_node, query_client) = network.start_query_node(db_opt).await;
7808
7809        let deadline = Instant::now() + Duration::from_secs(180);
7810        let start = wait_for_state_start::<D>(&db, deadline).await?;
7811        let state_start = D::state_start_height(&db).await?;
7812        check_state_start(expected, start, state_start, pruned, tip)?;
7813        if expected == EmptyStateStart::RecentBoundary && !stamp_pruned_height {
7814            let (first_pruned, first_block) = D::data_heights(&db).await?;
7815            ensure!(
7816                first_pruned.is_some() && first_pruned == first_block.map(|height| height - 1),
7817                "a fresh pruned database marked {first_pruned:?} as pruned, but its first block \
7818                 is {first_block:?}"
7819            );
7820        }
7821        let past = start.max(pruned) + PRUNED_START_EPOCH_HEIGHT;
7822        wait_for_state_head_above::<D>(&db, past, start, deadline).await?;
7823        let lowest = D::lowest_block_tree_node(&db).await?;
7824        ensure!(
7825            lowest == start,
7826            "state below the start {start} was computed, from {lowest}"
7827        );
7828
7829        wait_until_block_height(&network.api_client, "fee-state/block-height", past + 1).await;
7830        match expected {
7831            EmptyStateStart::RecentBoundary => {
7832                check_fee_state_from_start(&query_client, start, past, network.fee_recipient)
7833                    .await?
7834            },
7835            EmptyStateStart::Genesis => {
7836                let fee_state = format!("fee-state/{past}/{}", network.fee_recipient);
7837                assert_same_response(&network.api_client, &query_client, &fee_state).await?;
7838            },
7839        }
7840        Ok(())
7841    }
7842
7843    /// The epoch height of the network in [`test_state_loop_starts_on_pruned_data`].
7844    const PRUNED_START_EPOCH_HEIGHT: u64 = 10;
7845
7846    /// The network of [`test_state_loop_starts_on_pruned_data`]. Node 0 is the API node, the only
7847    /// peer the query node fetches the snapshot, leaves and accounts from. Node 1 is shut down, to
7848    /// come back as the query node on its own database, with no query data.
7849    struct PrunedStartNetwork<D: TestableSqlDataSource> {
7850        network: TestNetwork<D::Options, 5>,
7851        storage: Vec<D::Storage>,
7852        /// Node 1's consensus persistence.
7853        persistence: D::Options,
7854        api_url: Url,
7855        api_client: Client<ClientErr, StaticVersion<0, 1>>,
7856        genesis_state: ValidatedState,
7857        fee_recipient: FeeAccount,
7858    }
7859
7860    impl<D: TestableSqlDataSource> PrunedStartNetwork<D> {
7861        async fn start() -> anyhow::Result<Self> {
7862            let storage = join_all((0..5).map(|_| D::create_storage())).await;
7863            let persistence: [_; 5] = storage
7864                .iter()
7865                .map(D::persistence_options)
7866                .collect::<Vec<_>>()
7867                .try_into()
7868                .unwrap();
7869            let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7870            let api_url: Url = format!("http://localhost:{api_port}").parse().unwrap();
7871            let config = TestNetworkConfigBuilder::with_num_nodes()
7872                .api_config(D::options(
7873                    &storage[0],
7874                    Options::with_port(api_port).catchup(Default::default()),
7875                ))
7876                .network_config(
7877                    TestConfigBuilder::default()
7878                        .epoch_height(PRUNED_START_EPOCH_HEIGHT)
7879                        .build(),
7880                )
7881                .persistences(persistence.clone())
7882                .catchups(std::array::from_fn(|_| api_state_peers(&api_url)))
7883                .pos_hook(
7884                    DelegationConfig::MultipleDelegators,
7885                    StakeTableContractVersion::V3,
7886                    TEST_UPGRADE,
7887                )
7888                .await?
7889                .build();
7890            let genesis_state = config.states()[0].clone();
7891            let fee_recipient = genesis_state
7892                .chain_config
7893                .resolve()
7894                .context("genesis chain config")?
7895                .fee_recipient;
7896            let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
7897            network.peers[0].shut_down().await;
7898            network.peers.remove(0);
7899            let api_client = Client::new(api_url.clone());
7900            assert!(api_client.connect(Some(Duration::from_secs(60))).await);
7901            Ok(Self {
7902                network,
7903                storage,
7904                persistence: persistence[1].clone(),
7905                api_url,
7906                api_client,
7907                genesis_state,
7908                fee_recipient,
7909            })
7910        }
7911
7912        /// The options of node 1's database, with pruning on when `expected` is a recent boundary,
7913        /// and a connection to it.
7914        async fn query_database(
7915            &self,
7916            expected: EmptyStateStart,
7917        ) -> anyhow::Result<(persistence::sql::Options, D::SqlStorage)> {
7918            // When pruning is on, the pruner never runs within the test.
7919            const RETENTION: &str = "1000000s";
7920            let mut db_opt = D::tmp_options(&self.storage[1]);
7921            db_opt.prune = expected == EmptyStateStart::RecentBoundary;
7922            db_opt.pruning = <persistence::sql::PruningOptions as clap::Parser>::parse_from([
7923                "pruning",
7924                "--target-retention",
7925                RETENTION,
7926                "--state-target-retention",
7927                RETENTION,
7928                "--interval",
7929                "3600s",
7930            ]);
7931            let db = D::connect(&db_opt, StorageConnectionType::Query).await?;
7932            Ok((db_opt, db))
7933        }
7934
7935        /// Start node 1 as a query node on `db_opt`, and a client for its API.
7936        async fn start_query_node(
7937            &self,
7938            db_opt: persistence::sql::Options,
7939        ) -> (
7940            SequencerContext<<D::Options as PersistenceOptions>::Persistence>,
7941            Client<ClientErr, StaticVersion<0, 1>>,
7942        ) {
7943            let query_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
7944            let query_node = serve_query_node(
7945                self.network.cfg.clone(),
7946                self.genesis_state.clone(),
7947                self.persistence.clone(),
7948                db_opt,
7949                Options::with_port(query_port),
7950                vec![self.api_url.clone()],
7951                self.api_url.clone(),
7952            )
7953            .await;
7954            let query_client =
7955                Client::new(format!("http://localhost:{query_port}").parse().unwrap());
7956            (query_node, query_client)
7957        }
7958    }
7959
7960    /// Start node 1 of the network `cfg` as a query node on `db_opt`. It serves the API `api`, plus
7961    /// the query module. Its availability fetcher asks `availability_peers`, and its state catchup
7962    /// the API node at `api_url` alone.
7963    async fn serve_query_node<const N: usize, P: PersistenceOptions>(
7964        cfg: TestConfig<N>,
7965        genesis_state: ValidatedState,
7966        node_persistence: P,
7967        db_opt: persistence::sql::Options,
7968        api: Options,
7969        availability_peers: Vec<Url>,
7970        api_url: Url,
7971    ) -> SequencerContext<P::Persistence> {
7972        let opt = api.query_sql(
7973            Query {
7974                peers: availability_peers,
7975                ..Default::default()
7976            },
7977            db_opt,
7978        );
7979        let state_peers = api_state_peers(&api_url);
7980        let query_node = opt
7981            .serve(move |metrics, consumer, storage| {
7982                async move {
7983                    Ok(cfg
7984                        .init_node(
7985                            1,
7986                            genesis_state,
7987                            node_persistence,
7988                            Some(state_peers),
7989                            storage,
7990                            &*metrics,
7991                            STAKE_TABLE_CAPACITY_FOR_TEST,
7992                            consumer,
7993                            TEST_UPGRADE,
7994                            Default::default(),
7995                        )
7996                        .await)
7997                }
7998                .boxed()
7999            })
8000            .await
8001            .expect("query node should start");
8002        query_node.start_consensus().await;
8003        query_node
8004    }
8005
8006    /// State peers that are just the API node at `api_url`.
8007    fn api_state_peers(api_url: &Url) -> StatePeers<StaticVersion<0, 1>> {
8008        StatePeers::from_urls(
8009            vec![api_url.clone()],
8010            Default::default(),
8011            Duration::from_secs(2),
8012            &NoMetrics,
8013        )
8014    }
8015
8016    /// The height the state loop started from, once it has stored its first snapshot, which holds
8017    /// the lowest block tree nodes.
8018    async fn wait_for_state_start<D: TestableSqlDataSource>(
8019        db: &D::SqlStorage,
8020        deadline: Instant,
8021    ) -> anyhow::Result<u64> {
8022        while D::state_heights(db).await?.1 == 0 {
8023            ensure!(Instant::now() < deadline, "state loop did not start");
8024            sleep(Duration::from_millis(200)).await;
8025        }
8026        D::lowest_block_tree_node(db).await
8027    }
8028
8029    /// Check that the state loop started where `expected` says, given the data pruned height and
8030    /// the tip when the query node started, and that it recorded a start from a boundary.
8031    fn check_state_start(
8032        expected: EmptyStateStart,
8033        start: u64,
8034        state_start: Option<u64>,
8035        pruned: u64,
8036        tip: u64,
8037    ) -> anyhow::Result<()> {
8038        let recorded = (expected == EmptyStateStart::RecentBoundary).then_some(start);
8039        ensure!(
8040            state_start == recorded,
8041            "the recorded state start is {state_start:?}, not {recorded:?}"
8042        );
8043        match expected {
8044            // The boundary is recent only if the node's first local block was the consensus tip,
8045            // not a lower block it filled in first.
8046            EmptyStateStart::RecentBoundary => ensure!(
8047                start > pruned
8048                    && start.is_multiple_of(PRUNED_START_EPOCH_HEIGHT)
8049                    && start + PRUNED_START_EPOCH_HEIGHT > tip,
8050                "state loop started at {start}, not at an epoch boundary above the data pruned \
8051                 height {pruned} and within an epoch of the tip {tip} when the node started"
8052            ),
8053            // The genesis tree has no blocks, so the first block tree nodes are at height 1.
8054            EmptyStateStart::Genesis => {
8055                ensure!(start == 1, "state loop started at {start}, not at genesis")
8056            },
8057        }
8058        Ok(())
8059    }
8060
8061    async fn wait_for_state_head_above<D: TestableSqlDataSource>(
8062        db: &D::SqlStorage,
8063        past: u64,
8064        start: u64,
8065        deadline: Instant,
8066    ) -> anyhow::Result<()> {
8067        loop {
8068            let (_, head) = D::state_heights(db).await?;
8069            if head > past {
8070                return Ok(());
8071            }
8072            ensure!(
8073                Instant::now() < deadline,
8074                "state loop did not get past {past}: head {head}, started at {start}"
8075            );
8076            sleep(Duration::from_millis(200)).await;
8077        }
8078    }
8079
8080    /// Check the fee state a node that started from the boundary `start` serves: the snapshot holds
8081    /// no fee account, and no block since has read one, so the state is missing at the boundary and
8082    /// at `past` above it, and pruned below it.
8083    async fn check_fee_state_from_start(
8084        client: &Client<ClientErr, StaticVersion<0, 1>>,
8085        start: u64,
8086        past: u64,
8087        account: FeeAccount,
8088    ) -> anyhow::Result<()> {
8089        for (height, status) in [
8090            (start, StatusCode::NOT_FOUND),
8091            (past, StatusCode::NOT_FOUND),
8092            (start - 1, StatusCode::GONE),
8093        ] {
8094            // The state query reads the header there, which the node may still be fetching.
8095            client
8096                .get::<serde_json::Value>(&format!("availability/header/{height}"))
8097                .send()
8098                .await?;
8099            let path = format!("fee-state/{height}/{account}");
8100            let answer = client
8101                .get::<serde_json::Value>(&path)
8102                .send()
8103                .await
8104                .map_err(|err| err.status);
8105            ensure!(
8106                answer == Err(status),
8107                "{path}: answered {answer:?}, not {status}"
8108            );
8109        }
8110        Ok(())
8111    }
8112
8113    /// Forward connections on `port` to `target`, so a peer's URL is dead until this runs and
8114    /// reachable from then on.
8115    async fn forward_tcp(port: u16, target: u16) {
8116        let listener = TcpListener::bind(("127.0.0.1", port)).await.unwrap();
8117        loop {
8118            let (mut inbound, _) = listener.accept().await.unwrap();
8119            tokio::spawn(async move {
8120                let mut outbound = TcpStream::connect(("127.0.0.1", target)).await.unwrap();
8121                _ = tokio::io::copy_bidirectional(&mut inbound, &mut outbound).await;
8122            });
8123        }
8124    }
8125
8126    #[apply(sql_data_sources)]
8127    async fn test_state_reconstruction<D: TestableSqlDataSource>(
8128        _d: PhantomData<D>,
8129    ) -> anyhow::Result<()> {
8130        // This test verifies that a query node can successfully reconstruct its state
8131        // after being shut down from the database
8132        //
8133        // Steps:
8134        // 1. Start a test network with 5 nodes.
8135        // 2. Add a query node connected to the network.
8136        // 3. Let the network run until 3 epochs have passed.
8137        // 4. Shut down the query node.
8138        // 5. Attempt to reconstruct its state from storage using:
8139        //    - No fee/reward accounts
8140        //    - Only fee accounts
8141        //    - Only reward accounts
8142        //    - Both fee and reward accounts
8143        // 6. Assert that the reconstructed state is correct in all scenarios.
8144
8145        const EPOCH_HEIGHT: u64 = 10;
8146
8147        let network_config = TestConfigBuilder::default()
8148            .epoch_height(EPOCH_HEIGHT)
8149            .build();
8150
8151        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
8152
8153        tracing::info!("API PORT = {api_port}");
8154        const NUM_NODES: usize = 5;
8155
8156        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
8157
8158        let config = TestNetworkConfigBuilder::with_num_nodes()
8159            .api_config(D::options(&storage[0], Options::with_port(api_port)))
8160            .network_config(network_config)
8161            .persistences(persistence.clone())
8162            .build();
8163        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
8164        let state = network.genesis_state(0);
8165        // Remove peer 0 and restart it with the query module enabled.
8166        // Adding an additional node to the test network is not straight forward,
8167        // as the keys have already been initialized in the config above.
8168        // So, we remove this node and re-add it using the same index.
8169        // Await the shutdown so the node's cliquenet listener releases its
8170        // port before the replacement node binds the same address.
8171        network.peers[0].shut_down().await;
8172        network.peers.remove(0);
8173
8174        let node_0_storage = &storage[1];
8175        let node_0_persistence = persistence[1].clone();
8176        let node_0_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
8177        tracing::info!("node_0_port {node_0_port}");
8178        let opt = Options::with_port(node_0_port).query_sql(
8179            Query {
8180                peers: vec![format!("http://localhost:{api_port}").parse().unwrap()],
8181                ..Query::test()
8182            },
8183            D::tmp_options(node_0_storage),
8184        );
8185        let node_0 = opt
8186            .clone()
8187            .serve(|metrics, consumer, storage| {
8188                let cfg = network.cfg.clone();
8189                let node_0_persistence = node_0_persistence.clone();
8190                let state = state.clone();
8191                async move {
8192                    Ok(cfg
8193                        .init_node(
8194                            1,
8195                            state,
8196                            node_0_persistence.clone(),
8197                            Some(StatePeers::<StaticVersion<0, 1>>::from_urls(
8198                                vec![format!("http://localhost:{api_port}").parse().unwrap()],
8199                                Default::default(),
8200                                Duration::from_secs(2),
8201                                &NoMetrics,
8202                            )),
8203                            storage,
8204                            &*metrics,
8205                            test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
8206                            consumer,
8207                            TEST_UPGRADE,
8208                            Default::default(),
8209                        )
8210                        .await)
8211                }
8212                .boxed()
8213            })
8214            .await
8215            .unwrap();
8216        node_0.start_consensus().await;
8217
8218        let mut events = network.peers[2].event_stream();
8219        // Wait until at least 3 epochs have passed
8220        wait_for_epochs(&mut events, EPOCH_HEIGHT, 3).await;
8221
8222        let instance = node_0.node_state();
8223        // A node can decide a leaf before it has validated that leaf's state itself.
8224        let state = timeout(Duration::from_secs(60), async {
8225            loop {
8226                if let Some(state) = node_0.decided_state().await {
8227                    break state;
8228                }
8229                sleep(Duration::from_millis(100)).await;
8230            }
8231        })
8232        .await
8233        .context("node 0 never validated a decided state")?;
8234        let fee_accounts = state
8235            .fee_merkle_tree
8236            .clone()
8237            .into_iter()
8238            .map(|(acct, _)| acct)
8239            .collect::<Vec<_>>();
8240        let reward_accounts = state
8241            .reward_merkle_tree_v2
8242            .clone()
8243            .into_iter()
8244            .map(|(acct, _)| acct)
8245            .collect::<Vec<_>>();
8246
8247        // At 0.6 a leaf can be decided before the next view's proposal arrives, and decided
8248        // proposals are pruned from consensus storage, so shut down only once an undecided
8249        // proposal is stored for `reconstruct_state` to reach.
8250        let persistence = node_0.persistence();
8251        timeout(Duration::from_secs(60), async {
8252            loop {
8253                let decided_view = node_0.decided_leaf().await.view_number();
8254                let proposals = persistence.load_quorum_proposals().await?;
8255                if proposals.keys().any(|view| *view > decided_view) {
8256                    break anyhow::Ok(());
8257                }
8258                sleep(Duration::from_millis(100)).await;
8259            }
8260        })
8261        .await
8262        .context("no undecided proposal was stored")??;
8263
8264        tracing::warn!("shutting down node 0");
8265        node_0.shutdown_consensus().await;
8266
8267        // The decide processor keeps pruning the proposals of decides that landed before the
8268        // shutdown, so wait for the stored set to settle before choosing one.
8269        let proposals = timeout(Duration::from_secs(30), async {
8270            let mut proposals = persistence.load_quorum_proposals().await?;
8271            loop {
8272                sleep(Duration::from_millis(500)).await;
8273                let now = persistence.load_quorum_proposals().await?;
8274                if now.keys().eq(proposals.keys()) {
8275                    break anyhow::Ok(now);
8276                }
8277                proposals = now;
8278            }
8279        })
8280        .await
8281        .context("stored proposals did not settle after the shutdown")??;
8282        let (to_view, proposal) = proposals
8283            .into_iter()
8284            .next()
8285            .expect("an undecided proposal survives the shutdown");
8286        let from_height = proposal.data.block_header().height() - 1;
8287
8288        let client: Client<ClientErr, SequencerApiVersion> =
8289            Client::new(format!("http://localhost:{node_0_port}").parse().unwrap());
8290        client.connect(Some(Duration::from_secs(10))).await;
8291        wait_until_block_height(&client, "node/block-height", from_height + 1).await;
8292        // `reconstruct_state` loads accounts from the merklized state at `from_height`, which a
8293        // separate loop writes after the block is indexed.
8294        wait_until_block_height(&client, "fee-state/block-height", from_height).await;
8295
8296        let ds = D::connect(
8297            &D::tmp_options(node_0_storage),
8298            StorageConnectionType::Sequencer,
8299        )
8300        .await
8301        .unwrap();
8302
8303        let (state, leaf) = D::reconstruct_state(&ds, &instance, from_height, to_view, &[], &[])
8304            .await
8305            .unwrap();
8306        assert_eq!(leaf.view_number(), to_view);
8307        assert!(
8308            state
8309                .block_merkle_tree
8310                .lookup(from_height)
8311                .expect_ok()
8312                .is_ok(),
8313            "inconsistent block merkle tree"
8314        );
8315
8316        // Reconstruct fee state
8317        let (state, leaf) =
8318            D::reconstruct_state(&ds, &instance, from_height, to_view, &fee_accounts, &[])
8319                .await
8320                .unwrap();
8321
8322        assert_eq!(leaf.view_number(), to_view);
8323        assert!(
8324            state
8325                .block_merkle_tree
8326                .lookup(from_height)
8327                .expect_ok()
8328                .is_ok(),
8329            "inconsistent block merkle tree"
8330        );
8331
8332        for account in &fee_accounts {
8333            state.fee_merkle_tree.lookup(account).expect_ok().unwrap();
8334        }
8335
8336        // Reconstruct reward state
8337
8338        let (state, leaf) =
8339            D::reconstruct_state(&ds, &instance, from_height, to_view, &[], &reward_accounts)
8340                .await
8341                .unwrap();
8342
8343        for account in &reward_accounts {
8344            state
8345                .reward_merkle_tree_v2
8346                .lookup(account)
8347                .expect_ok()
8348                .unwrap();
8349        }
8350
8351        assert_eq!(leaf.view_number(), to_view);
8352        assert!(
8353            state
8354                .block_merkle_tree
8355                .lookup(from_height)
8356                .expect_ok()
8357                .is_ok(),
8358            "inconsistent block merkle tree"
8359        );
8360        // Reconstruct reward and fee state
8361
8362        let (state, leaf) = D::reconstruct_state(
8363            &ds,
8364            &instance,
8365            from_height,
8366            to_view,
8367            &fee_accounts,
8368            &reward_accounts,
8369        )
8370        .await
8371        .unwrap();
8372
8373        assert!(
8374            state
8375                .block_merkle_tree
8376                .lookup(from_height)
8377                .expect_ok()
8378                .is_ok(),
8379            "inconsistent block merkle tree"
8380        );
8381        assert_eq!(leaf.view_number(), to_view);
8382
8383        for account in &reward_accounts {
8384            state
8385                .reward_merkle_tree_v2
8386                .lookup(account)
8387                .expect_ok()
8388                .unwrap();
8389        }
8390
8391        for account in &fee_accounts {
8392            state.fee_merkle_tree.lookup(account).expect_ok().unwrap();
8393        }
8394
8395        Ok(())
8396    }
8397
8398    #[apply(sql_data_sources)]
8399    async fn test_block_reward_api<D: TestableSqlDataSource>(
8400        _d: PhantomData<D>,
8401    ) -> anyhow::Result<()> {
8402        let epoch_height = 10;
8403
8404        let network_config = TestConfigBuilder::default()
8405            .epoch_height(epoch_height)
8406            .build();
8407
8408        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
8409
8410        const NUM_NODES: usize = 1;
8411        // Initialize nodes.
8412        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
8413
8414        let config = TestNetworkConfigBuilder::with_num_nodes()
8415            .api_config(D::options(&storage[0], Options::with_port(api_port)))
8416            .network_config(network_config.clone())
8417            .persistences(persistence.clone())
8418            .pos_hook(
8419                DelegationConfig::VariableAmounts,
8420                StakeTableContractVersion::V3,
8421                TEST_UPGRADE,
8422            )
8423            .await
8424            .unwrap()
8425            .build();
8426
8427        let network = TestNetwork::new(config, TEST_UPGRADE).await;
8428        let mut events = network.server.event_stream();
8429        let client: Client<ClientErr, SequencerApiVersion> =
8430            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
8431
8432        let _blocks = client
8433            .socket("availability/stream/blocks/0")
8434            .subscribe::<BlockQueryData<SeqTypes>>()
8435            .await
8436            .unwrap()
8437            .take(3)
8438            .try_collect::<Vec<_>>()
8439            .await
8440            .unwrap();
8441
8442        let block_reward = client
8443            .get::<Option<RewardAmount>>("node/block-reward")
8444            .send()
8445            .await
8446            .expect("failed to get block reward")
8447            .expect("block reward is None");
8448        tracing::info!("block_reward={block_reward:?}");
8449
8450        assert!(block_reward.0 > U256::ZERO);
8451
8452        let v2_block_reward: serde_json::Value = client
8453            .get("v2/node/block-reward")
8454            .send()
8455            .await
8456            .expect("failed to get v2 block reward");
8457        assert_eq!(
8458            v2_block_reward,
8459            serde_json::json!({"amount": block_reward.0.to_string()})
8460        );
8461
8462        // An epoch the chain has not reached has no committee and so no reward, which is what
8463        // makes this the probe that `epoch` reaches the per-epoch lookup at all: dropping the
8464        // parameter falls back to the fixed reward asserted above, and that is not empty.
8465        const UNREACHED_EPOCH: u64 = 1_000_000;
8466        let v1_epoch_reward = client
8467            .get::<Option<RewardAmount>>(&format!("node/block-reward/epoch/{UNREACHED_EPOCH}"))
8468            .send()
8469            .await
8470            .expect("failed to get v1 block reward for epoch");
8471        assert!(v1_epoch_reward.is_none(), "{v1_epoch_reward:?}");
8472        let v2_epoch_reward: serde_json::Value = client
8473            .get(&format!("v2/node/block-reward?epoch={UNREACHED_EPOCH}"))
8474            .send()
8475            .await
8476            .expect("failed to get v2 block reward for epoch");
8477        assert_eq!(v2_epoch_reward, serde_json::json!({}));
8478
8479        // This is the only harness that registers validators, so it is the only place the
8480        // validator and participation mappings meet real data.
8481        let (epoch, _) =
8482            wait_for_committee(&client, &mut events, epoch_height, 1, 5, |validators| {
8483                !validators.is_empty()
8484            })
8485            .await;
8486        let v1_validators: serde_json::Value = client
8487            .get(&format!("node/validators/{epoch}"))
8488            .send()
8489            .await
8490            .expect("failed to get v1 validators");
8491        let v1_validators = v1_validators.as_object().expect("a map of validators");
8492        assert!(!v1_validators.is_empty());
8493        let v2_validators: espresso_api::proto::ValidatorsResponse = client
8494            .get(&format!("v2/node/validators?epoch={epoch}"))
8495            .send()
8496            .await
8497            .expect("failed to get v2 validators");
8498        assert_eq!(v2_validators.validators.len(), v1_validators.len());
8499        for v2 in &v2_validators.validators {
8500            // v1 keys each validator by the account the entry itself carries.
8501            let v1 = &v1_validators[&v2.account];
8502            assert_eq!(v2.stake, v1["stake"].as_str().unwrap());
8503            assert_eq!(v2.commission, v1["commission"].as_u64().unwrap() as u32);
8504            assert_eq!(v2.authenticated, v1["authenticated"].as_bool().unwrap());
8505            assert_eq!(
8506                v2.stake_table_key.as_ref().map(|key| key.key.as_str()),
8507                v1["stake_table_key"].as_str()
8508            );
8509            assert_eq!(
8510                v2.state_ver_key.as_ref().map(|key| key.key.as_str()),
8511                v1["state_ver_key"].as_str()
8512            );
8513            let v1_delegators = v1["delegators"].as_object().unwrap();
8514            assert_eq!(v2.delegators.len(), v1_delegators.len());
8515            for delegator in &v2.delegators {
8516                assert_eq!(
8517                    delegator.amount,
8518                    v1_delegators[&delegator.account].as_str().unwrap()
8519                );
8520            }
8521        }
8522
8523        let v1_page: serde_json::Value = client
8524            .get(&format!("node/all-validators/{epoch}/0/1000"))
8525            .send()
8526            .await
8527            .expect("failed to get the v1 validator page");
8528        let v2_page: espresso_api::proto::ValidatorsResponse = client
8529            .get(&format!(
8530                "v2/node/all-validators?epoch={epoch}&offset=0&limit=1000"
8531            ))
8532            .send()
8533            .await
8534            .expect("failed to get the v2 validator page");
8535        let v1_page = v1_page.as_array().unwrap();
8536        assert!(!v1_page.is_empty());
8537        assert_eq!(
8538            v2_page
8539                .validators
8540                .iter()
8541                .map(|validator| validator.account.as_str())
8542                .collect::<Vec<_>>(),
8543            v1_page
8544                .iter()
8545                .map(|validator| validator["account"].as_str().unwrap())
8546                .collect::<Vec<_>>()
8547        );
8548
8549        // Walking one row at a time is what pins `offset` against `limit`: transposed, the page
8550        // never moves. Only reachable with more than one registered validator.
8551        for (offset, v1_row) in v1_page.iter().enumerate() {
8552            let v2_row: espresso_api::proto::ValidatorsResponse = client
8553                .get(&format!(
8554                    "v2/node/all-validators?epoch={epoch}&offset={offset}&limit=1"
8555                ))
8556                .send()
8557                .await
8558                .expect("failed to get a v2 validator row");
8559            assert_eq!(
8560                v2_row.validators.first().map(|v| v.account.as_str()),
8561                Some(v1_row["account"].as_str().unwrap()),
8562                "offset {offset}"
8563            );
8564        }
8565
8566        // v1 refuses this as a bad request, so v2 must not report it as an internal error.
8567        let v1_err = client
8568            .get::<serde_json::Value>(&format!("node/all-validators/{epoch}/0/1001"))
8569            .send()
8570            .await
8571            .unwrap_err();
8572        let v2_err = client
8573            .get::<serde_json::Value>(&format!(
8574                "v2/node/all-validators?epoch={epoch}&offset=0&limit=1001"
8575            ))
8576            .send()
8577            .await
8578            .unwrap_err();
8579        assert_eq!(v1_err.status, StatusCode::BAD_REQUEST, "{v1_err}");
8580        assert_eq!(v2_err.status, v1_err.status, "{v2_err}");
8581
8582        // Omitting a required parameter is refused rather than read as epoch or limit zero.
8583        for route in [
8584            "v2/node/validators",
8585            "v2/node/all-validators?epoch=1&offset=0",
8586            "v2/node/header-window?start_time=0",
8587        ] {
8588            let err = client
8589                .get::<serde_json::Value>(route)
8590                .send()
8591                .await
8592                .unwrap_err();
8593            assert_eq!(err.status, StatusCode::BAD_REQUEST, "{route}: {err}");
8594        }
8595
8596        // Proposal participation is the arm the shorter test cannot reach, and comparing it here
8597        // catches a handler that delegates to the vote method instead.
8598        let v1_proposals: serde_json::Value = client
8599            .get("node/participation/proposal/current")
8600            .send()
8601            .await
8602            .expect("failed to get v1 proposal participation");
8603        let v2_proposals: espresso_api::proto::ParticipationResponse = client
8604            .get("v2/node/participation/proposal")
8605            .send()
8606            .await
8607            .expect("failed to get v2 proposal participation");
8608        let v1_proposals = v1_proposals.as_object().unwrap();
8609        assert_eq!(v2_proposals.participation.len(), v1_proposals.len());
8610        for entry in &v2_proposals.participation {
8611            let key = &entry.key.as_ref().unwrap().key;
8612            assert_eq!(entry.participation, v1_proposals[key].as_f64().unwrap());
8613        }
8614
8615        Ok(())
8616    }
8617
8618    /// `chain_id`: None = default (35353, non-mainnet), Some(1) = mainnet
8619    #[rstest]
8620    #[case(None)]
8621    #[case(Some(1u64))]
8622    #[test_log::test(tokio::test(flavor = "multi_thread"))]
8623    async fn test_token_supply_api<D: TestableSqlDataSource>(
8624        #[values(PhantomData::<Postgres>, PhantomData::<Sqlite>)] _d: PhantomData<D>,
8625        #[case] chain_id: Option<u64>,
8626    ) -> anyhow::Result<()> {
8627        use alloy::primitives::utils::parse_ether;
8628        use espresso_types::v0_3::ChainConfig;
8629
8630        let epoch_height = 10;
8631        let network_config = TestConfigBuilder::default()
8632            .epoch_height(epoch_height)
8633            .build();
8634
8635        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
8636
8637        const NUM_NODES: usize = 1;
8638        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
8639
8640        // Use the real initial supply (3.59B tokens) so the unlock schedule
8641        // produces realistic locked/unlocked values in the supply calculations.
8642        let initial_supply_tokens = U256::from(3_590_000_000u64);
8643        let initial_supply_wei = parse_ether("3590000000").unwrap();
8644
8645        let mut builder = TestNetworkConfigBuilder::with_num_nodes()
8646            .api_config(D::options(&storage[0], Options::with_port(api_port)))
8647            .network_config(network_config.clone())
8648            .persistences(persistence.clone())
8649            .initial_token_supply(initial_supply_tokens);
8650
8651        // The stake table deploy in `pos_hook` keeps chain_id from state[0].
8652        if let Some(id) = chain_id {
8653            let state = ValidatedState {
8654                chain_config: ChainConfig {
8655                    chain_id: U256::from(id).into(),
8656                    ..Default::default()
8657                }
8658                .into(),
8659                ..Default::default()
8660            };
8661            builder = builder.states(std::array::from_fn(|_| state.clone()));
8662        }
8663
8664        let config = builder
8665            .pos_hook(
8666                DelegationConfig::VariableAmounts,
8667                StakeTableContractVersion::V3,
8668                TEST_UPGRADE,
8669            )
8670            .await
8671            .unwrap()
8672            .build();
8673
8674        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
8675        let client: Client<ClientErr, SequencerApiVersion> =
8676            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
8677
8678        let _blocks = client
8679            .socket("availability/stream/blocks/0")
8680            .subscribe::<BlockQueryData<SeqTypes>>()
8681            .await
8682            .unwrap()
8683            .take(3)
8684            .try_collect::<Vec<_>>()
8685            .await
8686            .unwrap();
8687
8688        let minted: String = client
8689            .get("token/total-minted-supply")
8690            .send()
8691            .await
8692            .expect("total-minted-supply");
8693        let circ_eth: String = client
8694            .get("token/circulating-supply-ethereum")
8695            .send()
8696            .await
8697            .expect("circulating-supply-ethereum");
8698        let circulating: String = client
8699            .get("token/circulating-supply")
8700            .send()
8701            .await
8702            .expect("circulating-supply");
8703        tracing::info!(%minted, %circ_eth, %circulating);
8704
8705        let minted = parse_ether(&minted)?;
8706        let circ_eth = parse_ether(&circ_eth)?;
8707        let circ = parse_ether(&circulating)?;
8708
8709        assert_eq!(minted, initial_supply_wei);
8710        assert!(circ_eth <= minted);
8711        assert!(circ >= circ_eth);
8712        assert!(circ > U256::ZERO);
8713
8714        if chain_id == Some(1) {
8715            // Proves the unlock schedule is hooked up: locked > 0 means
8716            // the mainnet code path ran. Vesting ends ~2032; delete after.
8717            assert!(circ_eth < minted);
8718        }
8719
8720        Ok(())
8721    }
8722
8723    #[apply(sql_data_sources)]
8724    async fn test_scanning_token_contract_initialized_event<D: TestableSqlDataSource>(
8725        _d: PhantomData<D>,
8726    ) -> anyhow::Result<()> {
8727        use espresso_types::v0_3::ChainConfig;
8728
8729        let blocks_per_epoch = 10;
8730
8731        let network_config = TestConfigBuilder::<1>::default()
8732            .epoch_height(blocks_per_epoch)
8733            .build();
8734
8735        let (genesis_state, genesis_stake) = light_client_genesis_from_stake_table(
8736            &network_config.hotshot_config().hotshot_stake_table(),
8737            STAKE_TABLE_CAPACITY_FOR_TEST,
8738        )
8739        .unwrap();
8740
8741        let deployer = ProviderBuilder::new()
8742            .wallet(EthereumWallet::from(network_config.signer().clone()))
8743            .connect_http(network_config.l1_url().clone());
8744
8745        let mut contracts = Contracts::new();
8746        let args = DeployerArgsBuilder::default()
8747            .deployer(deployer.clone())
8748            .rpc_url(network_config.l1_url().clone())
8749            .mock_light_client(true)
8750            .genesis_lc_state(genesis_state)
8751            .genesis_st_state(genesis_stake)
8752            .blocks_per_epoch(blocks_per_epoch)
8753            .epoch_start_block(1)
8754            .multisig_pauser(network_config.signer().address())
8755            .token_name("Espresso".to_string())
8756            .token_symbol("ESP".to_string())
8757            .initial_token_supply(U256::from(3590000000u64))
8758            .ops_timelock_delay(U256::from(0))
8759            .ops_timelock_admin(network_config.signer().address())
8760            .ops_timelock_proposers(vec![network_config.signer().address()])
8761            .ops_timelock_executors(vec![network_config.signer().address()])
8762            .safe_exit_timelock_delay(U256::from(0))
8763            .safe_exit_timelock_admin(network_config.signer().address())
8764            .safe_exit_timelock_proposers(vec![network_config.signer().address()])
8765            .safe_exit_timelock_executors(vec![network_config.signer().address()])
8766            .build()
8767            .unwrap();
8768
8769        args.deploy_to_stake_table_v3(&mut contracts).await.unwrap();
8770
8771        let st_addr = contracts
8772            .address(Contract::StakeTableProxy)
8773            .expect("StakeTableProxy deployed");
8774
8775        let l1_url = network_config.l1_url().clone();
8776
8777        let storage = D::create_storage().await;
8778        let mut opt = <D as TestableSequencerDataSource>::persistence_options(&storage);
8779        let persistence = opt.create().await.unwrap();
8780
8781        let l1_client = L1ClientOptions {
8782            stake_table_update_interval: Duration::from_secs(7),
8783            l1_retry_delay: Duration::from_millis(10),
8784            l1_events_max_block_range: 10000,
8785            ..Default::default()
8786        }
8787        .connect(vec![l1_url])
8788        .unwrap();
8789        l1_client.spawn_tasks().await;
8790
8791        let fetcher = Fetcher::new(
8792            Arc::new(NullStateCatchup::default()),
8793            Arc::new(Mutex::new(persistence.clone())),
8794            l1_client.clone(),
8795            ChainConfig {
8796                stake_table_contract: Some(st_addr),
8797                base_fee: 0.into(),
8798                ..Default::default()
8799            },
8800        );
8801
8802        let provider = l1_client.provider;
8803        let stake_table = StakeTableV3::new(st_addr, provider.clone());
8804
8805        let stake_table_init_block = stake_table
8806            .initializedAtBlock()
8807            .block(BlockId::finalized())
8808            .call()
8809            .await?
8810            .to::<u64>();
8811
8812        tracing::info!("stake table init block = {stake_table_init_block}");
8813
8814        let token_address = stake_table
8815            .token()
8816            .block(BlockId::finalized())
8817            .call()
8818            .await
8819            .context("Failed to get token address")?;
8820
8821        let token = EspToken::new(token_address, provider.clone());
8822
8823        let init_log = fetcher
8824            .scan_token_contract_initialized_event_log(stake_table_init_block, token.clone())
8825            .await
8826            .unwrap();
8827
8828        let init_block = init_log.block_number.context("missing block number")?;
8829        let init_tx_hash = init_log
8830            .transaction_hash
8831            .context("missing transaction hash")?;
8832
8833        let transfer_logs = token
8834            .Transfer_filter()
8835            .from_block(init_block)
8836            .to_block(init_block)
8837            .query()
8838            .await
8839            .unwrap();
8840
8841        let (mint_transfer, _) = transfer_logs
8842            .iter()
8843            .find(|(transfer, log)| {
8844                log.transaction_hash == Some(init_tx_hash) && transfer.from == Address::ZERO
8845            })
8846            .context("no mint transfer event in init tx")?;
8847
8848        assert!(mint_transfer.value > U256::ZERO);
8849
8850        Ok(())
8851    }
8852
8853    #[apply(sql_data_sources)]
8854    async fn test_tx_metadata<D: TestableSqlDataSource>(_d: PhantomData<D>) {
8855        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
8856
8857        let url = format!("http://localhost:{port}").parse().unwrap();
8858        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
8859
8860        let storage = D::create_storage().await;
8861        let network_config = TestConfigBuilder::default().build();
8862        let config = TestNetworkConfigBuilder::default()
8863            .api_config(
8864                D::options(&storage, Options::with_port(port))
8865                    .submit(Default::default())
8866                    .explorer(Default::default()),
8867            )
8868            .network_config(network_config)
8869            .build();
8870        let network = TestNetwork::new(config, TEST_UPGRADE).await;
8871        let mut events = network.server.event_stream();
8872
8873        client.connect(None).await;
8874
8875        // Submit a few transactions in different namespaces.
8876        let namespace_counts = [(101, 1), (102, 2), (103, 3)];
8877        for (ns, count) in &namespace_counts {
8878            for i in 0..*count {
8879                let ns_id = NamespaceId::from(*ns as u64);
8880                let txn = Transaction::new(ns_id, vec![*ns, i]);
8881                client
8882                    .post::<()>("submit/submit")
8883                    .body_json(&txn)
8884                    .unwrap()
8885                    .send()
8886                    .await
8887                    .unwrap();
8888                let (block, _) = wait_for_decide_on_handle(&mut events, &txn).await;
8889                // The decide can reach us before the query service has the block's payload.
8890                client
8891                    .socket(&format!("availability/stream/blocks/{block}"))
8892                    .subscribe::<BlockQueryData<SeqTypes>>()
8893                    .await
8894                    .unwrap()
8895                    .next()
8896                    .await
8897                    .unwrap()
8898                    .unwrap();
8899
8900                // Block summary should contain information about the namespace.
8901                let summary: BlockSummaryQueryData<SeqTypes> = client
8902                    .get(&format!("availability/block/summary/{block}"))
8903                    .send()
8904                    .await
8905                    .unwrap();
8906                let ns_info = summary.namespaces();
8907                assert_eq!(ns_info.len(), 1);
8908                assert_eq!(ns_info.keys().copied().collect::<Vec<_>>(), vec![ns_id]);
8909                assert_eq!(ns_info[&ns_id].num_transactions, 1);
8910                assert_eq!(ns_info[&ns_id].size, txn.size_in_block(true));
8911            }
8912        }
8913
8914        // List transactions in each namespace.
8915        for (ns, count) in &namespace_counts {
8916            tracing::info!(ns, "list transactions in namespace");
8917
8918            let ns_id = NamespaceId::from(*ns as u64);
8919            let summaries: TransactionSummariesResponse<SeqTypes> = client
8920                .get(&format!(
8921                    "explorer/transactions/latest/{count}/namespace/{ns_id}"
8922                ))
8923                .send()
8924                .await
8925                .unwrap();
8926            let txs = summaries.transaction_summaries;
8927            assert_eq!(txs.len(), *count as usize);
8928
8929            // Check that transactions are listed in descending order.
8930            for i in 0..*count {
8931                let summary = &txs[i as usize];
8932                let expected = Transaction::new(ns_id, vec![*ns, count - i - 1]);
8933                assert_eq!(summary.rollups, vec![ns_id]);
8934                assert_eq!(summary.hash, expected.commit());
8935            }
8936        }
8937    }
8938
8939    /// The v2 endpoints adapt the v1 handlers, so on one node both versions must report the same
8940    /// values, with v2's query parameters selecting what v1's path parameters do.
8941    #[apply(sql_data_sources)]
8942    async fn test_v2_api_agrees_with_v1<D: TestableSqlDataSource>(_d: PhantomData<D>) {
8943        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
8944
8945        let url = format!("http://localhost:{port}").parse().unwrap();
8946        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
8947
8948        // At 0.6 the query service gets decided payloads from the DA proposals in consensus
8949        // storage, and proactive fetching is off, so the nodes need real persistence.
8950        let (storage, persistence) = node_storage::<D, 5>().await;
8951        let network_config = TestConfigBuilder::default().build();
8952        let mut ds_opts = D::tmp_options(&storage[0]);
8953        ds_opts.disable_proactive_fetching = true;
8954        let config = TestNetworkConfigBuilder::default()
8955            .api_config(
8956                Options::with_port(port)
8957                    .query_sql(Default::default(), ds_opts)
8958                    .submit(Default::default())
8959                    .config(Default::default())
8960                    .explorer(Default::default()),
8961            )
8962            .network_config(network_config)
8963            .persistences(persistence)
8964            .build();
8965
8966        let network = TestNetwork::new(config, TEST_UPGRADE).await;
8967        let mut events = network.server.event_stream();
8968
8969        client.connect(None).await;
8970
8971        let namespace_counts = [(101u8, 1u8), (102, 2)];
8972        let mut blocks = Vec::new();
8973        for (ns, count) in namespace_counts {
8974            for i in 0..count {
8975                let txn = Transaction::new(NamespaceId::from(u64::from(ns)), vec![ns, i]);
8976                client
8977                    .post::<()>("submit/submit")
8978                    .body_json(&txn)
8979                    .unwrap()
8980                    .send()
8981                    .await
8982                    .unwrap();
8983                let (block, _) = wait_for_decide_on_handle(&mut events, &txn).await;
8984                blocks.push(block);
8985            }
8986        }
8987        let first_block = blocks[0];
8988        let last_block = *blocks.last().unwrap();
8989
8990        // The counts come from aggregates a background task fills in after each block is
8991        // stored, so wait for them to reach the last submitted transaction first. Nothing else
8992        // submits before the counts are compared, so they are stable from then on.
8993        let expected_total: u64 = namespace_counts
8994            .iter()
8995            .map(|(_, count)| u64::from(*count))
8996            .sum();
8997        let total = tokio::time::timeout(Duration::from_secs(60), async {
8998            loop {
8999                let count: u64 = client.get("node/transactions/count").send().await.unwrap();
9000                if count >= expected_total {
9001                    return count;
9002                }
9003                sleep(Duration::from_millis(200)).await;
9004            }
9005        })
9006        .await
9007        .expect("transaction count never caught up");
9008        assert_eq!(total, expected_total);
9009
9010        let v2_total: serde_json::Value = client
9011            .get("v2/node/transaction-count")
9012            .send()
9013            .await
9014            .unwrap();
9015        assert_eq!(
9016            v2_total,
9017            serde_json::json!({"count": expected_total.to_string()})
9018        );
9019
9020        // Each range below leaves a transaction out, so the strict inequalities are what give the
9021        // comparisons teeth: a handler that dropped `from` or `to` would answer with the
9022        // chain-wide total instead, and only those assertions notice.
9023        let v1_through_first: u64 = client
9024            .get(&format!("node/transactions/count/{first_block}"))
9025            .send()
9026            .await
9027            .unwrap();
9028        assert!(
9029            v1_through_first < expected_total,
9030            "every transaction landed in one block {blocks:?}, so no range excludes one"
9031        );
9032        let v2_through_first: serde_json::Value = client
9033            .get(&format!("v2/node/transaction-count?to={first_block}"))
9034            .send()
9035            .await
9036            .unwrap();
9037        assert_eq!(
9038            v2_through_first,
9039            serde_json::json!({"count": v1_through_first.to_string()})
9040        );
9041
9042        let v1_last_only: u64 = client
9043            .get(&format!(
9044                "node/transactions/count/{last_block}/{last_block}"
9045            ))
9046            .send()
9047            .await
9048            .unwrap();
9049        assert!(v1_last_only < expected_total, "{blocks:?}");
9050        let v2_last_only: serde_json::Value = client
9051            .get(&format!(
9052                "v2/node/transaction-count?from={last_block}&to={last_block}"
9053            ))
9054            .send()
9055            .await
9056            .unwrap();
9057        assert_eq!(
9058            v2_last_only,
9059            serde_json::json!({"count": v1_last_only.to_string()})
9060        );
9061
9062        let v1_total_size: u64 = client.get("node/payloads/size").send().await.unwrap();
9063        let v2_total_size: serde_json::Value =
9064            client.get("v2/node/payload-size").send().await.unwrap();
9065        assert_eq!(
9066            v2_total_size,
9067            serde_json::json!({"size": v1_total_size.to_string()})
9068        );
9069
9070        let v1_block_size: u64 = client
9071            .get(&format!("node/payloads/size/{last_block}/{last_block}"))
9072            .send()
9073            .await
9074            .unwrap();
9075        assert!(v1_block_size < v1_total_size, "{blocks:?}");
9076        let v2_block_size: serde_json::Value = client
9077            .get(&format!(
9078                "v2/node/payload-size?from={last_block}&to={last_block}"
9079            ))
9080            .send()
9081            .await
9082            .unwrap();
9083        assert_eq!(
9084            v2_block_size,
9085            serde_json::json!({"size": v1_block_size.to_string()})
9086        );
9087
9088        for (ns, count) in namespace_counts {
9089            let v1_count: u64 = client
9090                .get(&format!("node/transactions/count/namespace/{ns}"))
9091                .send()
9092                .await
9093                .unwrap();
9094            assert_eq!(v1_count, count as u64);
9095            let v2_count: serde_json::Value = client
9096                .get(&format!("v2/node/transaction-count?namespace={ns}"))
9097                .send()
9098                .await
9099                .unwrap();
9100            assert_eq!(v2_count, serde_json::json!({"count": v1_count.to_string()}));
9101
9102            let v1_size: u64 = client
9103                .get(&format!("node/payloads/size/namespace/{ns}"))
9104                .send()
9105                .await
9106                .unwrap();
9107            let v2_size: serde_json::Value = client
9108                .get(&format!("v2/node/payload-size?namespace={ns}"))
9109                .send()
9110                .await
9111                .unwrap();
9112            assert_eq!(v2_size, serde_json::json!({"size": v1_size.to_string()}));
9113        }
9114
9115        // The query service caches sync status for minutes, so a fresh node still reports its
9116        // startup snapshot here; what is checked is that v2 relays exactly what v1 reports.
9117        let v1_sync: hotshot_query_service::node::SyncStatusQueryData =
9118            client.get("node/sync-status").send().await.unwrap();
9119        let v2_sync: espresso_api::proto::SyncStatusResponse =
9120            client.get("v2/node/sync-status").send().await.unwrap();
9121        assert!(!v1_sync.blocks.ranges.is_empty(), "{v1_sync:?}");
9122        for (what, v1, v2) in [
9123            ("blocks", &v1_sync.blocks, v2_sync.blocks.unwrap()),
9124            ("leaves", &v1_sync.leaves, v2_sync.leaves.unwrap()),
9125            (
9126                "vid_common",
9127                &v1_sync.vid_common,
9128                v2_sync.vid_common.unwrap(),
9129            ),
9130        ] {
9131            assert!(!v1.ranges.is_empty(), "{what}: {v1:?}");
9132            assert_eq!(v2.missing, v1.missing as u64, "{what}");
9133            assert_eq!(v2.ranges.len(), v1.ranges.len(), "{what}");
9134            for (v1, v2) in v1.ranges.iter().zip(&v2.ranges) {
9135                assert_eq!((v2.start, v2.end), (v1.start as u64, v1.end as u64));
9136                let expected = match v1.status {
9137                    hotshot_query_service::node::SyncStatus::Present => {
9138                        espresso_api::proto::SyncStatus::Present
9139                    },
9140                    hotshot_query_service::node::SyncStatus::Missing => {
9141                        espresso_api::proto::SyncStatus::Missing
9142                    },
9143                    hotshot_query_service::node::SyncStatus::Pruned => {
9144                        espresso_api::proto::SyncStatus::Pruned
9145                    },
9146                };
9147                assert_eq!(v2.status(), expected);
9148            }
9149        }
9150        assert_eq!(
9151            v2_sync.pruned_height,
9152            v1_sync.pruned_height.map(|height| height as u64)
9153        );
9154
9155        let v1_limits: serde_json::Value = client.get("node/limits").send().await.unwrap();
9156        let v2_limits: espresso_api::proto::NodeLimitsResponse =
9157            client.get("v2/node/limits").send().await.unwrap();
9158        assert_eq!(
9159            v2_limits.window_limit,
9160            v1_limits["window_limit"].as_u64().unwrap()
9161        );
9162
9163        for (v1_route, v2_route) in [("node/stake-table/current", "v2/node/stake-table")] {
9164            let v1_table: serde_json::Value = client.get(v1_route).send().await.unwrap();
9165            let v2_table: espresso_api::proto::StakeTableResponse =
9166                client.get(v2_route).send().await.unwrap();
9167            assert_eq!(v2_table.epoch, v1_table["epoch"].as_u64());
9168            let v1_peers = v1_table["stake_table"].as_array().unwrap();
9169            assert!(!v1_peers.is_empty(), "{v1_table}");
9170            assert_eq!(v2_table.stake_table.len(), v1_peers.len());
9171            for (v1_peer, v2_peer) in v1_peers.iter().zip(&v2_table.stake_table) {
9172                let v1_entry = &v1_peer["stake_table_entry"];
9173                let v2_entry = v2_peer.stake_table_entry.as_ref().unwrap();
9174                assert_eq!(
9175                    v2_entry.stake_key.as_ref().unwrap().key,
9176                    v1_entry["stake_key"].as_str().unwrap()
9177                );
9178                assert_eq!(
9179                    v2_entry.stake_amount,
9180                    v1_entry["stake_amount"].as_str().unwrap()
9181                );
9182                assert_eq!(
9183                    v2_peer.state_ver_key.as_ref().unwrap().key,
9184                    v1_peer["state_ver_key"].as_str().unwrap()
9185                );
9186                match (&v2_peer.connect_info, v1_peer["connect_info"].as_object()) {
9187                    (Some(v2_info), Some(v1_info)) => {
9188                        assert_eq!(v2_info.p2p_addr, v1_info["p2p_addr"].as_str().unwrap());
9189                        let v1_key = bs58::decode(v1_info["x25519_key"].as_str().unwrap())
9190                            .into_vec()
9191                            .unwrap();
9192                        assert_eq!(
9193                            v2_info.x25519_key.parse::<x25519::PublicKey>().unwrap(),
9194                            x25519::PublicKey::try_from(&v1_key[..]).unwrap()
9195                        );
9196                    },
9197                    (None, None) => {},
9198                    (v2_info, v1_info) => panic!("{v2_info:?} against {v1_info:?}"),
9199                }
9200            }
9201        }
9202
9203        // An epoch this network never reaches, so only the status is comparable.
9204        let v1_err = client
9205            .get::<serde_json::Value>("node/stake-table/1000000")
9206            .send()
9207            .await
9208            .unwrap_err();
9209        let v2_err = client
9210            .get::<serde_json::Value>("v2/node/stake-table?epoch=1000000")
9211            .send()
9212            .await
9213            .unwrap_err();
9214        assert_eq!(v2_err.status, v1_err.status);
9215
9216        // The signer covers only the newest leaf of each decide and runs behind the query
9217        // storage, so search back from the tip for a height v1 answers rather than naming one.
9218        let block_height: u64 = client.get("status/block-height").send().await.unwrap();
9219        let (height, v1_signature) = {
9220            let mut found = None;
9221            for height in (1..block_height).rev().take(50) {
9222                // As raw JSON, so the v2 strings are compared against the bytes v1 serves rather
9223                // than against a `Display` impl that could disagree with its own serde.
9224                match client
9225                    .get::<serde_json::Value>(&format!("state-signature/block/{height}"))
9226                    .send()
9227                    .await
9228                {
9229                    Ok(body) => {
9230                        found = Some((height, body));
9231                        break;
9232                    },
9233                    Err(err) if err.status == StatusCode::NOT_FOUND => {},
9234                    Err(err) => panic!("v1 state-signature/block/{height}: {err:?}"),
9235                }
9236            }
9237            found.expect("no recent block carries a state signature")
9238        };
9239        let v2_signature: espresso_api::proto::StateSignatureResponse = client
9240            .get(&format!("v2/state-signature/block?height={height}"))
9241            .send()
9242            .await
9243            .unwrap();
9244        assert_eq!(
9245            v1_signature
9246                .as_object()
9247                .unwrap()
9248                .keys()
9249                .map(String::as_str)
9250                .collect::<BTreeSet<_>>(),
9251            BTreeSet::from([
9252                "auth_root",
9253                "key",
9254                "next_stake",
9255                "signature",
9256                "state",
9257                "v2_signature",
9258            ]),
9259            "v1 grew a field `StateSignatureResponse` does not carry"
9260        );
9261        assert_eq!(v2_signature.key, v1_signature["key"].as_str().unwrap());
9262        assert_eq!(
9263            v2_signature.light_client_state,
9264            v1_signature["state"].as_str().unwrap()
9265        );
9266        assert_eq!(
9267            v2_signature.next_stake_table_state,
9268            v1_signature["next_stake"].as_str().unwrap()
9269        );
9270        assert_eq!(
9271            v2_signature.auth_root,
9272            v1_signature["auth_root"].as_str().unwrap()
9273        );
9274        assert_eq!(
9275            v2_signature.lcv3_signature,
9276            v1_signature["signature"].as_str().unwrap()
9277        );
9278        assert_eq!(
9279            v2_signature.lcv2_signature,
9280            v1_signature["v2_signature"].as_str().unwrap()
9281        );
9282
9283        // The height is required rather than defaulted to the genesis block.
9284        let err = client
9285            .get::<serde_json::Value>("v2/state-signature/block")
9286            .send()
9287            .await
9288            .unwrap_err();
9289        assert_eq!(err.status, StatusCode::BAD_REQUEST);
9290
9291        // A height the node never signed is a 404 on both versions.
9292        let unsigned = block_height + 1000;
9293        let v1_err = client
9294            .get::<serde_json::Value>(&format!("state-signature/block/{unsigned}"))
9295            .send()
9296            .await
9297            .unwrap_err();
9298        let v2_err = client
9299            .get::<serde_json::Value>(&format!("v2/state-signature/block?height={unsigned}"))
9300            .send()
9301            .await
9302            .unwrap_err();
9303        assert_eq!(v1_err.status, StatusCode::NOT_FOUND);
9304        assert_eq!(v2_err.status, StatusCode::NOT_FOUND);
9305
9306        // Waited on rather than just compared, so a handler that returns the commitment without
9307        // sequencing anything fails here.
9308        let submitted = Transaction::new(NamespaceId::from(103u64), vec![103, 0]);
9309        let accepted: espresso_api::proto::SubmitTransactionResponse = client
9310            .post("v2/submit/transaction")
9311            .body_json(&espresso_api::proto::SubmitTransactionRequest {
9312                namespace: Some(103),
9313                payload: Some(vec![103, 0]),
9314            })
9315            .unwrap()
9316            .send()
9317            .await
9318            .unwrap();
9319        assert_eq!(accepted.hash, submitted.commit().to_string());
9320        wait_for_decide_on_handle(&mut events, &submitted).await;
9321
9322        let oversized = u64::from(network.server.node_state().chain_config.max_block_size) + 1;
9323        let oversized = vec![0u8; oversized as usize];
9324        let bad_submissions = [
9325            (
9326                "missing namespace",
9327                serde_json::json!({ "payload": "" }),
9328                espresso_api::proto::SubmitTransactionRequest {
9329                    namespace: None,
9330                    payload: Some(vec![]),
9331                },
9332            ),
9333            (
9334                "missing payload",
9335                serde_json::json!({ "namespace": 104 }),
9336                espresso_api::proto::SubmitTransactionRequest {
9337                    namespace: Some(104),
9338                    payload: None,
9339                },
9340            ),
9341            (
9342                "namespace above u32::MAX",
9343                serde_json::json!({ "namespace": 1u64 << 32, "payload": "" }),
9344                espresso_api::proto::SubmitTransactionRequest {
9345                    namespace: Some(1 << 32),
9346                    payload: Some(vec![]),
9347                },
9348            ),
9349            (
9350                "payload above max_block_size",
9351                serde_json::to_value(Transaction::new(
9352                    NamespaceId::from(104u64),
9353                    oversized.clone(),
9354                ))
9355                .unwrap(),
9356                espresso_api::proto::SubmitTransactionRequest {
9357                    namespace: Some(104),
9358                    payload: Some(oversized),
9359                },
9360            ),
9361        ];
9362        for (case, v1_body, v2_body) in bad_submissions {
9363            let v1_err = client
9364                .post::<serde_json::Value>("submit/submit")
9365                .body_json(&v1_body)
9366                .unwrap()
9367                .send()
9368                .await
9369                .unwrap_err();
9370            let v2_err = client
9371                .post::<serde_json::Value>("v2/submit/transaction")
9372                .body_json(&v2_body)
9373                .unwrap()
9374                .send()
9375                .await
9376                .unwrap_err();
9377            assert_eq!(v1_err.status, StatusCode::BAD_REQUEST, "v1, {case}");
9378            assert_eq!(v2_err.status, StatusCode::BAD_REQUEST, "v2, {case}");
9379        }
9380
9381        // Every decided view moves these, so retry until a pair straddles no view.
9382        let (v1_votes, v2_votes) = {
9383            let mut attempts = 0;
9384            loop {
9385                let v1: serde_json::Value = client
9386                    .get("node/participation/vote/current")
9387                    .send()
9388                    .await
9389                    .unwrap();
9390                let v2: espresso_api::proto::ParticipationResponse = client
9391                    .get("v2/node/participation/vote")
9392                    .send()
9393                    .await
9394                    .unwrap();
9395                let v1: std::collections::BTreeMap<String, f64> = v1
9396                    .as_object()
9397                    .unwrap()
9398                    .iter()
9399                    .map(|(key, value)| (key.clone(), value.as_f64().unwrap()))
9400                    .collect();
9401                let v2_map: std::collections::BTreeMap<String, f64> = v2
9402                    .participation
9403                    .iter()
9404                    .map(|entry| (entry.key.as_ref().unwrap().key.clone(), entry.participation))
9405                    .collect();
9406                if v1 == v2_map {
9407                    break (v1, v2);
9408                }
9409                attempts += 1;
9410                assert!(attempts < 5, "v1 and v2 never agreed: {v1:?} vs {v2_map:?}");
9411            }
9412        };
9413        assert!(!v1_votes.is_empty());
9414        let keys: Vec<_> = v2_votes
9415            .participation
9416            .iter()
9417            .map(|entry| &entry.key.as_ref().unwrap().key)
9418            .collect();
9419        assert!(keys.windows(2).all(|pair| pair[0] <= pair[1]), "{keys:?}");
9420
9421        let height_before: u64 = client.get("node/block-height").send().await.unwrap();
9422        let v2_height: espresso_api::proto::NodeBlockHeightResponse =
9423            client.get("v2/node/block-height").send().await.unwrap();
9424        let height_after: u64 = client.get("node/block-height").send().await.unwrap();
9425        assert!(
9426            (height_before..=height_after).contains(&v2_height.height),
9427            "{} outside {height_before}..={height_after}",
9428            v2_height.height
9429        );
9430
9431        // End at a timestamp already passed, so blocks decided meanwhile fall outside the window.
9432        let tip: serde_json::Value = client
9433            .get("node/header/window/0/999999999999")
9434            .send()
9435            .await
9436            .unwrap();
9437        let end = tip["window"].as_array().unwrap().last().unwrap()["fields"]["timestamp"]
9438            .as_u64()
9439            .unwrap();
9440        assert!(end > 0, "{tip}");
9441        let v1_window: serde_json::Value = client
9442            .get(&format!("node/header/window/0/{end}"))
9443            .send()
9444            .await
9445            .unwrap();
9446        let v2_window: espresso_api::proto::HeaderWindowResponse = client
9447            .get(&format!("v2/node/header-window?start_time=0&end={end}"))
9448            .send()
9449            .await
9450            .unwrap();
9451        let v1_headers = v1_window["window"].as_array().unwrap();
9452        assert!(!v1_headers.is_empty(), "{v1_window}");
9453        assert_eq!(v2_window.window.len(), v1_headers.len());
9454        for (v1_header, v2_header) in v1_headers.iter().zip(&v2_window.window) {
9455            let v2_header = match v2_header.header.as_ref().unwrap() {
9456                espresso_api::proto::header_response::Header::V6(header) => header,
9457                other => panic!("this network runs 0.6, not {other:?}"),
9458            };
9459            // Versioned headers nest their fields under the version.
9460            let v1_header = &v1_header["fields"];
9461            assert_eq!(v2_header.height, v1_header["height"].as_u64().unwrap());
9462            assert_eq!(
9463                v2_header.payload_commitment,
9464                v1_header["payload_commitment"].as_str().unwrap()
9465            );
9466            assert_eq!(
9467                v2_header.builder_commitment,
9468                v1_header["builder_commitment"].as_str().unwrap()
9469            );
9470            assert_eq!(
9471                v2_header.block_merkle_tree_root,
9472                v1_header["block_merkle_tree_root"].as_str().unwrap()
9473            );
9474            assert_eq!(
9475                v2_header.fee_merkle_tree_root,
9476                v1_header["fee_merkle_tree_root"].as_str().unwrap()
9477            );
9478            assert_eq!(
9479                v2_header.timestamp,
9480                v1_header["timestamp"].as_u64().unwrap()
9481            );
9482            assert_eq!(
9483                v2_header.timestamp_millis,
9484                v1_header["timestamp_millis"].as_u64().unwrap()
9485            );
9486            assert_eq!(v2_header.l1_head, v1_header["l1_head"].as_u64().unwrap());
9487            assert_eq!(
9488                v2_header.reward_merkle_tree_root,
9489                v1_header["reward_merkle_tree_root"].as_str().unwrap()
9490            );
9491            assert_eq!(
9492                v2_header.total_reward_distributed,
9493                v1_header["total_reward_distributed"].as_str().unwrap()
9494            );
9495            let v2_fee = v2_header.fee_info.as_ref().unwrap();
9496            assert_eq!(
9497                v2_fee.account,
9498                v1_header["fee_info"]["account"].as_str().unwrap()
9499            );
9500            assert_eq!(
9501                v2_fee.amount,
9502                v1_header["fee_info"]["amount"].as_str().unwrap()
9503            );
9504            let v1_chain_config = &v1_header["chain_config"]["chain_config"]["Left"];
9505            let v2_chain_config = match v2_header
9506                .chain_config
9507                .as_ref()
9508                .unwrap()
9509                .chain_config
9510                .as_ref()
9511                .unwrap()
9512            {
9513                espresso_api::proto::resolvable_chain_config::ChainConfig::Full(config) => config,
9514                other => panic!("a test network header carries its config: {other:?}"),
9515            };
9516            assert_eq!(
9517                v2_chain_config.chain_id,
9518                v1_chain_config["chain_id"].as_str().unwrap()
9519            );
9520            assert_eq!(
9521                v2_chain_config.max_block_size.to_string(),
9522                v1_chain_config["max_block_size"].as_str().unwrap()
9523            );
9524            assert_eq!(
9525                v2_chain_config.base_fee,
9526                v1_chain_config["base_fee"].as_str().unwrap()
9527            );
9528            assert_eq!(
9529                v2_chain_config.fee_recipient,
9530                v1_chain_config["fee_recipient"].as_str().unwrap()
9531            );
9532            assert_eq!(
9533                v2_header.ns_table.as_ref().unwrap().bytes,
9534                base64::Engine::decode(
9535                    &base64::engine::general_purpose::STANDARD,
9536                    v1_header["ns_table"]["bytes"].as_str().unwrap()
9537                )
9538                .unwrap(),
9539            );
9540            assert_eq!(
9541                v2_header.l1_finalized.is_some(),
9542                !v1_header["l1_finalized"].is_null()
9543            );
9544            assert_eq!(
9545                v2_header.builder_signature.is_some(),
9546                !v1_header["builder_signature"].is_null()
9547            );
9548        }
9549        let v2_next = match v2_window.next.as_ref().unwrap().header.as_ref().unwrap() {
9550            espresso_api::proto::header_response::Header::V6(header) => header,
9551            other => panic!("this network runs 0.6, not {other:?}"),
9552        };
9553        assert_eq!(
9554            v2_next.height,
9555            v1_window["next"]["fields"]["height"].as_u64().unwrap()
9556        );
9557        // start_time=0 precedes every block, so like v1 the window has nothing before it.
9558        assert!(v1_window["prev"].is_null(), "{v1_window}");
9559        assert!(v2_window.prev.is_none());
9560
9561        // The other two selectors name the window by its first block, and each must agree with
9562        // the v1 route it mirrors. Starting at block 1 also gives `prev` something to hold.
9563        let first: espresso_types::Header = serde_json::from_value(v1_headers[1].clone()).unwrap();
9564        assert_eq!(first.height(), 1);
9565        let first_hash = committable::Committable::commit(&first);
9566        let height = |header: &espresso_api::proto::HeaderResponse| match header.header.as_ref() {
9567            Some(espresso_api::proto::header_response::Header::V6(header)) => header.height,
9568            other => panic!("this network runs 0.6, not {other:?}"),
9569        };
9570        for (v1_route, v2_query) in [
9571            (
9572                format!("node/header/window/from/1/{end}"),
9573                format!("start_height=1&end={end}"),
9574            ),
9575            (
9576                format!("node/header/window/from/hash/{first_hash}/{end}"),
9577                format!("start_hash={first_hash}&end={end}"),
9578            ),
9579        ] {
9580            let v1_window: serde_json::Value = client.get(&v1_route).send().await.unwrap();
9581            let v2_window: espresso_api::proto::HeaderWindowResponse = client
9582                .get(&format!("v2/node/header-window?{v2_query}"))
9583                .send()
9584                .await
9585                .unwrap();
9586            assert_eq!(
9587                v1_window["prev"]["fields"]["height"].as_u64(),
9588                Some(0),
9589                "{v1_route}"
9590            );
9591            assert_eq!(
9592                v2_window.window.iter().map(height).collect::<Vec<_>>(),
9593                v1_window["window"]
9594                    .as_array()
9595                    .unwrap()
9596                    .iter()
9597                    .map(|header| header["fields"]["height"].as_u64().unwrap())
9598                    .collect::<Vec<_>>(),
9599                "{v2_query}"
9600            );
9601            assert_eq!(
9602                v2_window.prev.as_ref().map(height),
9603                v1_window["prev"]["fields"]["height"].as_u64(),
9604                "{v2_query}"
9605            );
9606            assert_eq!(
9607                v2_window.next.as_ref().map(height),
9608                v1_window["next"]["fields"]["height"].as_u64(),
9609                "{v2_query}"
9610            );
9611        }
9612
9613        let v1_share: serde_json::Value = client.get("node/vid/share/1").send().await.unwrap();
9614        let v2_share: espresso_api::proto::VidShareResponse = client
9615            .get("v2/node/vid-share?height=1")
9616            .send()
9617            .await
9618            .unwrap();
9619        // 0.6 disperses with AvidmGf2, which v2 renders field for field from the share, so v2
9620        // must serve exactly the rendering of v1's share.
9621        let share: hotshot_types::data::VidShare =
9622            serde_json::from_value(v1_share.clone()).unwrap();
9623        assert!(
9624            matches!(share, hotshot_types::data::VidShare::V2(_)),
9625            "{v1_share}"
9626        );
9627        assert_eq!(
9628            v2_share,
9629            espresso_api::proto::VidShareResponse::try_from(&share).unwrap()
9630        );
9631
9632        // hash and payload_hash select the share by its block's hashes, as v1's own routes do, so
9633        // block 1's share comes back either way.
9634        let payload_hash = first.payload_commitment();
9635        for (v1_route, v2_query) in [
9636            (
9637                format!("node/vid/share/hash/{first_hash}"),
9638                format!("hash={first_hash}"),
9639            ),
9640            (
9641                format!("node/vid/share/payload-hash/{payload_hash}"),
9642                format!("payload_hash={payload_hash}"),
9643            ),
9644        ] {
9645            let v1: serde_json::Value = client.get(&v1_route).send().await.unwrap();
9646            assert_eq!(v1, v1_share, "{v1_route}");
9647            let v2: espresso_api::proto::VidShareResponse = client
9648                .get(&format!("v2/node/vid-share?{v2_query}"))
9649                .send()
9650                .await
9651                .unwrap();
9652            assert_eq!(v2, v2_share, "{v2_query}");
9653        }
9654
9655        // Naming the block by none or two of the selectors is refused, as is a hash that does
9656        // not parse; v1 has no route for the first two and answers the third with a 400.
9657        let v1_err = client
9658            .get::<serde_json::Value>("node/vid/share/hash/not-a-hash")
9659            .send()
9660            .await
9661            .unwrap_err();
9662        assert_eq!(v1_err.status, StatusCode::BAD_REQUEST, "{v1_err}");
9663        for query in [
9664            "v2/node/vid-share".to_string(),
9665            format!("v2/node/vid-share?height=1&hash={first_hash}"),
9666            "v2/node/vid-share?hash=not-a-hash".to_string(),
9667            format!("v2/node/header-window?end={end}"),
9668            format!("v2/node/header-window?start_time=0&start_height=1&end={end}"),
9669            format!("v2/node/header-window?start_hash=not-a-hash&end={end}"),
9670        ] {
9671            let err = client
9672                .get::<serde_json::Value>(&query)
9673                .send()
9674                .await
9675                .unwrap_err();
9676            assert_eq!(err.status, StatusCode::BAD_REQUEST, "{query}: {err}");
9677        }
9678
9679        // Compared as raw JSON, so the rendered amounts and timestamps are checked against the
9680        // bytes v1 serves rather than against the `Display` impls v2 reuses. Each comparison names
9681        // its target by height, since the latest block moves with every decide.
9682        let strings = |value: &serde_json::Value| {
9683            value
9684                .as_array()
9685                .unwrap()
9686                .iter()
9687                .map(|entry| entry.as_str().unwrap().to_string())
9688                .collect::<Vec<_>>()
9689        };
9690        let keys = |value: &serde_json::Value| {
9691            value
9692                .as_object()
9693                .unwrap()
9694                .keys()
9695                .cloned()
9696                .collect::<BTreeSet<_>>()
9697        };
9698        let key_set = |names: &[&str]| {
9699            names
9700                .iter()
9701                .map(|name| name.to_string())
9702                .collect::<BTreeSet<_>>()
9703        };
9704
9705        let v1_block: serde_json::Value = client
9706            .get(&format!("explorer/block/{first_block}"))
9707            .send()
9708            .await
9709            .unwrap();
9710        let v2_block: espresso_api::proto::ExplorerBlockDetailResponse = client
9711            .get(&format!("v2/explorer/block?height={first_block}"))
9712            .send()
9713            .await
9714            .unwrap();
9715        let v1_block = &v1_block["block_detail"];
9716        let v2_block = v2_block.block_detail.unwrap();
9717        assert_eq!(
9718            keys(v1_block),
9719            key_set(&[
9720                "block_reward",
9721                "fee_recipient",
9722                "hash",
9723                "height",
9724                "num_transactions",
9725                "proposer_id",
9726                "size",
9727                "time",
9728            ]),
9729            "v1 grew a field `ExplorerBlockDetail` does not carry"
9730        );
9731        assert_eq!(v2_block.hash, v1_block["hash"].as_str().unwrap());
9732        assert_eq!(v2_block.height, v1_block["height"].as_u64().unwrap());
9733        assert_eq!(v2_block.time, v1_block["time"].as_str().unwrap());
9734        assert_eq!(
9735            v2_block.num_transactions,
9736            v1_block["num_transactions"].as_u64().unwrap()
9737        );
9738        assert_eq!(v2_block.num_transactions, 1);
9739        assert_eq!(v2_block.size, v1_block["size"].as_u64().unwrap());
9740        assert_eq!(v2_block.proposer_id, strings(&v1_block["proposer_id"]));
9741        assert_eq!(v2_block.fee_recipient, strings(&v1_block["fee_recipient"]));
9742        assert_eq!(v2_block.block_reward, strings(&v1_block["block_reward"]));
9743        let by_hash: espresso_api::proto::ExplorerBlockDetailResponse = client
9744            .get(&format!("v2/explorer/block?hash={}", v2_block.hash))
9745            .send()
9746            .await
9747            .unwrap();
9748        assert_eq!(by_hash.block_detail.as_ref(), Some(&v2_block));
9749
9750        let assert_block_summary =
9751            |v1: &serde_json::Value, v2: &espresso_api::proto::ExplorerBlockSummary| {
9752                assert_eq!(
9753                    keys(v1),
9754                    key_set(&[
9755                        "hash",
9756                        "height",
9757                        "num_transactions",
9758                        "proposer_id",
9759                        "size",
9760                        "time",
9761                    ]),
9762                    "v1 grew a field `ExplorerBlockSummary` does not carry"
9763                );
9764                assert_eq!(v2.hash, v1["hash"].as_str().unwrap());
9765                assert_eq!(v2.height, v1["height"].as_u64().unwrap());
9766                assert_eq!(v2.proposer_id, strings(&v1["proposer_id"]));
9767                assert_eq!(
9768                    v2.num_transactions,
9769                    v1["num_transactions"].as_u64().unwrap()
9770                );
9771                assert_eq!(v2.size, v1["size"].as_u64().unwrap());
9772                assert_eq!(v2.time, v1["time"].as_str().unwrap());
9773            };
9774        let v1_blocks: serde_json::Value = client
9775            .get(&format!("explorer/blocks/{last_block}/3"))
9776            .send()
9777            .await
9778            .unwrap();
9779        let v2_blocks: espresso_api::proto::ExplorerBlockSummariesResponse = client
9780            .get(&format!("v2/explorer/blocks?height={last_block}&limit=3"))
9781            .send()
9782            .await
9783            .unwrap();
9784        let v1_blocks = v1_blocks["block_summaries"].as_array().unwrap();
9785        assert_eq!(v2_blocks.block_summaries.len(), v1_blocks.len());
9786        assert!(!v1_blocks.is_empty());
9787        for (v1, v2) in v1_blocks.iter().zip(&v2_blocks.block_summaries) {
9788            assert_block_summary(v1, v2);
9789        }
9790
9791        let assert_transaction_summary =
9792            |v1: &serde_json::Value, v2: &espresso_api::proto::ExplorerTransactionSummary| {
9793                assert_eq!(
9794                    keys(v1),
9795                    key_set(&[
9796                        "hash",
9797                        "height",
9798                        "num_transactions",
9799                        "offset",
9800                        "rollups",
9801                        "time",
9802                    ]),
9803                    "v1 grew a field `ExplorerTransactionSummary` does not carry"
9804                );
9805                assert_eq!(v2.hash, v1["hash"].as_str().unwrap());
9806                assert_eq!(
9807                    v2.rollups,
9808                    v1["rollups"]
9809                        .as_array()
9810                        .unwrap()
9811                        .iter()
9812                        .map(|namespace| namespace.as_u64().unwrap())
9813                        .collect::<Vec<_>>()
9814                );
9815                assert_eq!(v2.height, v1["height"].as_u64().unwrap());
9816                assert_eq!(v2.offset, v1["offset"].as_u64().unwrap());
9817                assert_eq!(
9818                    v2.num_transactions,
9819                    v1["num_transactions"].as_u64().unwrap()
9820                );
9821                assert_eq!(v2.time, v1["time"].as_str().unwrap());
9822            };
9823        let mut newest_transaction = None;
9824        for (v1_path, v2_path) in [
9825            (
9826                format!("explorer/transactions/from/{last_block}/0/5"),
9827                format!("v2/explorer/transactions?height={last_block}&offset=0&limit=5"),
9828            ),
9829            (
9830                format!("explorer/transactions/from/{last_block}/0/5/namespace/102"),
9831                format!(
9832                    "v2/explorer/transactions?height={last_block}&offset=0&limit=5&namespace=102"
9833                ),
9834            ),
9835            (
9836                format!("explorer/transactions/from/{last_block}/0/5/block/{first_block}"),
9837                format!(
9838                    "v2/explorer/transactions?height={last_block}&offset=0&limit=5&\
9839                     block={first_block}"
9840                ),
9841            ),
9842        ] {
9843            let v1: serde_json::Value = client.get(&v1_path).send().await.unwrap();
9844            let v2: espresso_api::proto::ExplorerTransactionSummariesResponse =
9845                client.get(&v2_path).send().await.unwrap();
9846            let v1 = v1["transaction_summaries"].as_array().unwrap();
9847            assert_eq!(v2.transaction_summaries.len(), v1.len(), "{v2_path}");
9848            assert!(!v1.is_empty(), "{v1_path}");
9849            for (v1, v2) in v1.iter().zip(&v2.transaction_summaries) {
9850                assert_transaction_summary(v1, v2);
9851            }
9852            newest_transaction.get_or_insert(v2.transaction_summaries[0].clone());
9853        }
9854
9855        let transaction = newest_transaction.unwrap();
9856        let v1_detail: serde_json::Value = client
9857            .get(&format!(
9858                "explorer/transaction/{}/{}",
9859                transaction.height, transaction.offset
9860            ))
9861            .send()
9862            .await
9863            .unwrap();
9864        let v2_detail: espresso_api::proto::ExplorerTransactionDetailResponse = client
9865            .get(&format!(
9866                "v2/explorer/transaction?height={}&offset={}",
9867                transaction.height, transaction.offset
9868            ))
9869            .send()
9870            .await
9871            .unwrap();
9872        let v1_detail = &v1_detail["transaction_detail"];
9873        let v1_details = &v1_detail["details"];
9874        let v2_details = v2_detail.details.as_ref().unwrap();
9875        assert_eq!(
9876            keys(v1_details),
9877            key_set(&[
9878                "block_confirmed",
9879                "fee_details",
9880                "hash",
9881                "height",
9882                "num_transactions",
9883                "offset",
9884                "sequencing_fees",
9885                "size",
9886                "time",
9887            ]),
9888            "v1 grew a field `ExplorerTransactionDetail` does not carry"
9889        );
9890        assert_eq!(v2_details.hash, transaction.hash);
9891        assert_eq!(v2_details.hash, v1_details["hash"].as_str().unwrap());
9892        assert_eq!(v2_details.height, v1_details["height"].as_u64().unwrap());
9893        assert_eq!(
9894            v2_details.block_confirmed,
9895            v1_details["block_confirmed"].as_bool().unwrap()
9896        );
9897        assert_eq!(v2_details.offset, v1_details["offset"].as_u64().unwrap());
9898        assert_eq!(
9899            v2_details.num_transactions,
9900            v1_details["num_transactions"].as_u64().unwrap()
9901        );
9902        assert_eq!(v2_details.size, v1_details["size"].as_u64().unwrap());
9903        assert_eq!(v2_details.time, v1_details["time"].as_str().unwrap());
9904        assert_eq!(
9905            v2_details.sequencing_fees,
9906            strings(&v1_details["sequencing_fees"])
9907        );
9908        assert_eq!(
9909            v2_details.fee_details.len(),
9910            v1_details["fee_details"].as_array().unwrap().len()
9911        );
9912        let v1_data: Vec<Transaction> = serde_json::from_value(v1_detail["data"].clone()).unwrap();
9913        assert!(!v1_data.is_empty());
9914        assert_eq!(
9915            v2_detail.data,
9916            v1_data
9917                .iter()
9918                .map(|transaction| espresso_api::proto::Transaction {
9919                    namespace: u64::from(transaction.namespace()),
9920                    payload: transaction.payload().to_vec(),
9921                })
9922                .collect::<Vec<_>>()
9923        );
9924        let by_hash: espresso_api::proto::ExplorerTransactionDetailResponse = client
9925            .get(&format!(
9926                "v2/explorer/transaction?hash={}",
9927                transaction.hash
9928            ))
9929            .send()
9930            .await
9931            .unwrap();
9932        assert_eq!(by_hash, v2_detail);
9933
9934        let v1_search: serde_json::Value = client
9935            .get(&format!("explorer/search/{}", transaction.hash))
9936            .send()
9937            .await
9938            .unwrap();
9939        let v2_search: espresso_api::proto::ExplorerSearchResponse = client
9940            .get(&format!("v2/explorer/search?query={}", transaction.hash))
9941            .send()
9942            .await
9943            .unwrap();
9944        let v1_found = v1_search["search_results"]["transactions"]
9945            .as_array()
9946            .unwrap();
9947        assert_eq!(v2_search.transactions.len(), v1_found.len());
9948        assert!(!v1_found.is_empty());
9949        for (v1, v2) in v1_found.iter().zip(&v2_search.transactions) {
9950            assert_transaction_summary(v1, v2);
9951        }
9952
9953        // v1 serves the histograms as four parallel arrays, which v2 zips into one point per
9954        // block, so this proves the zip lines the four values up with the right height. The two
9955        // reads can straddle a decide, so only the heights both windows hold are compared.
9956        let v1_summary: serde_json::Value = client
9957            .get("explorer/explorer-summary")
9958            .send()
9959            .await
9960            .unwrap();
9961        let v2_summary: espresso_api::proto::ExplorerSummaryResponse =
9962            client.get("v2/explorer/summary").send().await.unwrap();
9963        let v1_histograms = &v1_summary["explorer_summary"]["histograms"];
9964        let v1_points: HashMap<u64, usize> = v1_histograms["block_heights"]
9965            .as_array()
9966            .unwrap()
9967            .iter()
9968            .enumerate()
9969            .map(|(i, height)| (height.as_u64().unwrap(), i))
9970            .collect();
9971        let mut compared = 0;
9972        for point in &v2_summary.histograms {
9973            let Some(&i) = v1_points.get(&point.height) else {
9974                continue;
9975            };
9976            assert_eq!(point.block_time, v1_histograms["block_time"][i].as_f64());
9977            assert_eq!(point.block_size, v1_histograms["block_size"][i].as_u64());
9978            assert_eq!(
9979                point.block_transactions,
9980                v1_histograms["block_transactions"][i].as_u64().unwrap()
9981            );
9982            compared += 1;
9983        }
9984        assert!(compared > 0, "the two histogram windows share no height");
9985
9986        // Two ways of naming the same thing cannot both be given, and half of a height/offset
9987        // pair is a mistake rather than a fallback to the latest transaction.
9988        for query in [
9989            "v2/explorer/block?height=1&hash=BLOCK~aa",
9990            "v2/explorer/transaction?height=1",
9991            "v2/explorer/transactions?limit=1&block=1&namespace=1",
9992            "v2/explorer/transactions?limit=1&namespace=4294967296",
9993            "v2/explorer/blocks",
9994            "v2/explorer/search",
9995        ] {
9996            let err = client
9997                .get::<serde_json::Value>(query)
9998                .send()
9999                .await
10000                .unwrap_err();
10001            assert_eq!(err.status, StatusCode::BAD_REQUEST, "{query}");
10002        }
10003
10004        let missing_height = u64::from(u32::MAX);
10005        let foreign_tag = tagged_base64::TaggedBase64::new("FOO", &[0; 32]).unwrap();
10006        for (v1_path, v2_path, status) in [
10007            (
10008                format!("explorer/block/{missing_height}"),
10009                format!("v2/explorer/block?height={missing_height}"),
10010                StatusCode::NOT_FOUND,
10011            ),
10012            (
10013                format!("explorer/transaction/{missing_height}/0"),
10014                format!("v2/explorer/transaction?height={missing_height}&offset=0"),
10015                StatusCode::NOT_FOUND,
10016            ),
10017            (
10018                format!("explorer/transactions/from/1/{}/1", u64::MAX),
10019                format!(
10020                    "v2/explorer/transactions?height=1&offset={}&limit=1",
10021                    u64::MAX
10022                ),
10023                StatusCode::BAD_REQUEST,
10024            ),
10025            (
10026                format!("explorer/search/{foreign_tag}"),
10027                format!("v2/explorer/search?query={foreign_tag}"),
10028                StatusCode::BAD_REQUEST,
10029            ),
10030        ] {
10031            for path in [&v1_path, &v2_path] {
10032                let err = client
10033                    .get::<serde_json::Value>(path)
10034                    .send()
10035                    .await
10036                    .unwrap_err();
10037                assert_eq!(err.status, status, "{path}");
10038            }
10039        }
10040        let unknown_block = tagged_base64::TaggedBase64::new("BLOCK", &[0; 32]).unwrap();
10041        let v1_search: serde_json::Value = client
10042            .get(&format!("explorer/search/{unknown_block}"))
10043            .send()
10044            .await
10045            .unwrap();
10046        assert_eq!(
10047            v1_search["search_results"]["blocks"],
10048            serde_json::json!([]),
10049            "an unknown block hash is an empty result, as an unknown transaction hash is"
10050        );
10051        let v2_search: espresso_api::proto::ExplorerSearchResponse = client
10052            .get(&format!("v2/explorer/search?query={unknown_block}"))
10053            .send()
10054            .await
10055            .unwrap();
10056        assert!(v2_search.blocks.is_empty());
10057
10058        // A catching-up peer names state by its parent leaf's (height, view). The decided leaf's
10059        // state is always in memory, while the view after it may not have a proposal yet.
10060        let leaf = network.server.decided_leaf().await;
10061        let catchup_height = leaf.height();
10062        let catchup_view = leaf.view_number().u64();
10063        // The prefunded builder account, so the fee routes prove a presence. Its address has hex
10064        // letters, so lowercase and EIP-55 renderings differ.
10065        let account = format!(
10066            "{:x}",
10067            TestConfig::<5>::builder_key().fee_account().address()
10068        );
10069        let v1_account: crate::api::AccountQueryData = client
10070            .get(&format!(
10071                "catchup/{catchup_height}/{catchup_view}/account/{account}"
10072            ))
10073            .send()
10074            .await
10075            .unwrap();
10076        let v2_account: espresso_api::proto::FeeAccountProofResponse = client
10077            .get(&format!(
10078                "v2/catchup/fee-account?height={catchup_height}&view={catchup_view}&\
10079                 address=0x{account}"
10080            ))
10081            .send()
10082            .await
10083            .unwrap();
10084        // Compared as a value rather than a string, since v1 serves this `U256` as `0x` hex and
10085        // v2 as a decimal string.
10086        assert_eq!(
10087            v2_account.balance.parse::<U256>().unwrap(),
10088            v1_account.balance
10089        );
10090        assert_eq!(v2_account, v1_account.clone().into());
10091        let v2_proof = v2_account.proof.unwrap();
10092        assert_eq!(
10093            v2_proof.account,
10094            format!("0x{account}"),
10095            "the proof names the account in lowercase hex"
10096        );
10097        assert!(matches!(
10098            v2_proof.proof.unwrap().proof,
10099            Some(espresso_api::proto::fee_merkle_proof::Proof::Presence(_))
10100        ));
10101
10102        // Some catchup routes may have nothing to serve this early in the chain, so a route both
10103        // versions refuse must at least refuse with the same status.
10104        fn both_or_same_status<A, B>(
10105            route: &str,
10106            v1: Result<A, ClientErr>,
10107            v2: Result<B, ClientErr>,
10108        ) -> Option<(A, B)> {
10109            match (v1, v2) {
10110                (Ok(v1), Ok(v2)) => Some((v1, v2)),
10111                (Err(v1), Err(v2)) => {
10112                    assert_eq!(v2.status, v1.status, "{route}");
10113                    None
10114                },
10115                (v1, v2) => panic!("{route}: v1 ok {}, v2 ok {}", v1.is_ok(), v2.is_ok()),
10116            }
10117        }
10118
10119        let v1_frontier: crate::api::BlocksFrontier = client
10120            .get(&format!("catchup/{catchup_height}/{catchup_view}/blocks"))
10121            .send()
10122            .await
10123            .unwrap();
10124        let v2_frontier: espresso_api::proto::MerklePathResponse = client
10125            .get(&format!(
10126                "v2/catchup/blocks-frontier?height={catchup_height}&view={catchup_view}"
10127            ))
10128            .send()
10129            .await
10130            .unwrap();
10131        assert_eq!(
10132            v2_frontier,
10133            espresso_api::proto::MerklePathResponse::from(&v1_frontier)
10134        );
10135
10136        // v2's bincode trees are decoded as their type and compared with v1's through serde.
10137        let accounts = vec![format!("0x{account}")];
10138        let v1: espresso_types::FeeMerkleTree = client
10139            .post(&format!("catchup/{catchup_height}/{catchup_view}/accounts"))
10140            .body_json(&accounts)
10141            .unwrap()
10142            .send()
10143            .await
10144            .unwrap();
10145        let v2: espresso_api::proto::FeeMerkleTreeResponse = client
10146            .post("v2/catchup/fee-accounts")
10147            .body_json(&espresso_api::proto::GetCatchupFeeAccountsRequest {
10148                height: Some(catchup_height),
10149                view: Some(catchup_view),
10150                addresses: accounts.clone(),
10151            })
10152            .unwrap()
10153            .send()
10154            .await
10155            .unwrap();
10156        let v2: espresso_types::FeeMerkleTree = bincode::deserialize(&v2.tree).unwrap();
10157        assert_eq!(
10158            serde_json::to_value(&v2).unwrap(),
10159            serde_json::to_value(&v1).unwrap()
10160        );
10161        let v1 = client
10162            .post::<espresso_types::v0_3::RewardMerkleTreeV1>(&format!(
10163                "catchup/{catchup_height}/{catchup_view}/reward-accounts"
10164            ))
10165            .body_json(&accounts)
10166            .unwrap()
10167            .send()
10168            .await;
10169        let v2 = client
10170            .post::<espresso_api::proto::RewardMerkleTreeV1Response>("v2/catchup/reward-accounts")
10171            .body_json(&espresso_api::proto::GetCatchupRewardAccountsRequest {
10172                height: Some(catchup_height),
10173                view: Some(catchup_view),
10174                addresses: accounts,
10175            })
10176            .unwrap()
10177            .send()
10178            .await;
10179        if let Some((v1, v2)) = both_or_same_status("reward-accounts", v1, v2) {
10180            let v2: espresso_types::v0_3::RewardMerkleTreeV1 =
10181                bincode::deserialize(&v2.tree).unwrap();
10182            assert_eq!(
10183                serde_json::to_value(&v2).unwrap(),
10184                serde_json::to_value(&v1).unwrap()
10185            );
10186        }
10187
10188        let v1 = client
10189            .get::<espresso_types::v0_3::RewardAccountQueryDataV1>(&format!(
10190                "catchup/{catchup_height}/{catchup_view}/reward-account/{account}"
10191            ))
10192            .send()
10193            .await;
10194        let v2 = client
10195            .get::<espresso_api::proto::RewardAccountProofResponse>(&format!(
10196                "v2/catchup/reward-account?height={catchup_height}&view={catchup_view}&\
10197                 address=0x{account}"
10198            ))
10199            .send()
10200            .await;
10201        if let Some((v1, v2)) = both_or_same_status("reward-account", v1, v2) {
10202            assert_eq!(v2, v1.into());
10203        }
10204
10205        let v1 = client
10206            .get::<espresso_types::v0_4::RewardAccountQueryDataV2>(&format!(
10207                "catchup/{catchup_height}/{catchup_view}/reward-account-v2/{account}"
10208            ))
10209            .send()
10210            .await;
10211        let v2 = client
10212            .get::<espresso_api::proto::RewardAccountProofResponse>(&format!(
10213                "v2/catchup/reward-account-v2?height={catchup_height}&view={catchup_view}&\
10214                 address=0x{account}"
10215            ))
10216            .send()
10217            .await;
10218        if let Some((v1, v2)) = both_or_same_status("reward-account-v2", v1, v2) {
10219            assert_eq!(v2, v1.into());
10220        }
10221
10222        let v1 = client
10223            .get::<Vec<u8>>(&format!(
10224                "catchup/reward-merkle-tree-v2/{catchup_height}/{catchup_view}"
10225            ))
10226            .send()
10227            .await;
10228        let v2 = client
10229            .get::<espresso_api::proto::RewardMerkleTreeV2Response>(&format!(
10230                "v2/catchup/reward-merkle-tree-v2?height={catchup_height}&view={catchup_view}"
10231            ))
10232            .send()
10233            .await;
10234        if let Some((v1, v2)) = both_or_same_status("reward-merkle-tree-v2", v1, v2) {
10235            assert_eq!(v2.tree, v1);
10236        }
10237
10238        let v1_chain: Vec<espresso_types::Leaf2> = client
10239            .get(&format!("catchup/{catchup_height}/leafchain"))
10240            .send()
10241            .await
10242            .unwrap();
10243        let v2_chain: espresso_api::proto::CatchupLeafChainResponse = client
10244            .get(&format!("v2/catchup/leaf-chain?height={catchup_height}"))
10245            .send()
10246            .await
10247            .unwrap();
10248        assert!(!v1_chain.is_empty());
10249        assert_eq!(
10250            v2_chain.leaves,
10251            v1_chain.iter().map(Into::into).collect::<Vec<_>>()
10252        );
10253
10254        let v1 = client
10255            .get::<espresso_types::Certificate2<SeqTypes>>(&format!(
10256                "catchup/{catchup_height}/cert2"
10257            ))
10258            .send()
10259            .await;
10260        let v2 = client
10261            .get::<espresso_api::proto::Cert2Response>(&format!(
10262                "v2/catchup/cert2?height={catchup_height}"
10263            ))
10264            .send()
10265            .await;
10266        if let Some((v1, v2)) = both_or_same_status("cert2", v1, v2) {
10267            assert_eq!(v2.certificate, Some((&v1).into()));
10268        }
10269
10270        let v1 = client
10271            .get::<hotshot_types::simple_certificate::LightClientStateUpdateCertificateV2<SeqTypes>>(
10272                "catchup/1/state-cert",
10273            )
10274            .send()
10275            .await;
10276        let v2 = client
10277            .get::<espresso_api::proto::StateCertV2Response>("v2/catchup/state-cert?epoch=1")
10278            .send()
10279            .await;
10280        if let Some((v1, v2)) = both_or_same_status("state-cert", v1, v2) {
10281            assert_eq!(v2, (&espresso_types::v0_4::StateCertQueryDataV2(v1)).into());
10282        }
10283
10284        let known = leaf.block_header().chain_config().commit();
10285        let v1_config: espresso_types::v0_3::ChainConfig = client
10286            .get(&format!("catchup/chain-config/{known}"))
10287            .send()
10288            .await
10289            .unwrap();
10290        let v2_config: espresso_api::proto::CatchupChainConfigResponse = client
10291            .get(&format!("v2/catchup/chain-config?commitment={known}"))
10292            .send()
10293            .await
10294            .unwrap();
10295        assert_eq!(v2_config.chain_config, Some((&v1_config).into()));
10296        // A well-formed commitment of a config the node never saw, so both versions reach the
10297        // lookup rather than refusing to parse it.
10298        let unknown = espresso_types::v0_3::ChainConfig {
10299            chain_id: 999u64.into(),
10300            ..Default::default()
10301        }
10302        .commit();
10303        let v1_status = client
10304            .get::<serde_json::Value>(&format!("catchup/chain-config/{unknown}"))
10305            .send()
10306            .await
10307            .unwrap_err()
10308            .status;
10309        let v2_status = client
10310            .get::<serde_json::Value>(&format!("v2/catchup/chain-config?commitment={unknown}"))
10311            .send()
10312            .await
10313            .unwrap_err()
10314            .status;
10315        assert_ne!(v1_status, StatusCode::BAD_REQUEST);
10316        assert_eq!(v2_status, v1_status);
10317
10318        for query in [
10319            format!("v2/catchup/fee-account?view=1&address=0x{account}").as_str(),
10320            "v2/catchup/blocks-frontier?height=1",
10321            "v2/catchup/leaf-chain",
10322            "v2/catchup/cert2",
10323            "v2/catchup/chain-config",
10324            "v2/catchup/reward-account?height=1&view=1",
10325            format!("v2/catchup/reward-account-v2?view=1&address=0x{account}").as_str(),
10326            "v2/catchup/reward-merkle-tree-v2?height=1",
10327            "v2/catchup/state-cert",
10328        ] {
10329            let err = client
10330                .get::<serde_json::Value>(query)
10331                .send()
10332                .await
10333                .unwrap_err();
10334            assert_eq!(err.status, StatusCode::BAD_REQUEST, "{query}");
10335        }
10336        for (route, body) in [
10337            (
10338                "v2/catchup/fee-accounts",
10339                serde_json::json!({ "height": 1, "addresses": [] }),
10340            ),
10341            (
10342                "v2/catchup/fee-accounts",
10343                serde_json::json!({ "height": 1, "view": 1, "addresses": ["0xnot-hex"] }),
10344            ),
10345            (
10346                "v2/catchup/reward-accounts",
10347                serde_json::json!({ "view": 1, "addresses": [] }),
10348            ),
10349            (
10350                "v2/catchup/reward-accounts",
10351                serde_json::json!({ "height": 1, "view": 1, "addresses": ["0xnot-hex"] }),
10352            ),
10353        ] {
10354            let err = client
10355                .post::<serde_json::Value>(route)
10356                .body_json(&body)
10357                .unwrap()
10358                .send()
10359                .await
10360                .unwrap_err();
10361            assert_eq!(err.status, StatusCode::BAD_REQUEST, "{route} {body}");
10362        }
10363
10364        assert_v2_light_client_agrees_with_v1(
10365            &client,
10366            last_block,
10367            u64::from(namespace_counts[1].0),
10368        )
10369        .await;
10370
10371        let v1_config = client
10372            .get::<espresso_types::config::PublicNetworkConfig>("config/hotshot")
10373            .send()
10374            .await
10375            .unwrap();
10376        let v1_hotshot = v1_config.hotshot_config().into_hotshot_config();
10377        let v2_hotshot: espresso_api::proto::HotshotConfigResponse =
10378            client.get("v2/config/hotshot").send().await.unwrap();
10379        // The handler destructures HotShotConfig exhaustively, so a field it forgets to serve is
10380        // a compile error. What that cannot catch is a field wired to the wrong source or scaled
10381        // wrongly, so every field is compared here, especially the millisecond conversions.
10382        assert_eq!(
10383            v2_hotshot,
10384            espresso_api::proto::HotshotConfigResponse {
10385                start_threshold_numerator: v1_hotshot.start_threshold.0,
10386                start_threshold_denominator: v1_hotshot.start_threshold.1,
10387                num_nodes_with_stake: v1_hotshot.num_nodes_with_stake.get() as u64,
10388                da_staked_committee_size: v1_hotshot.da_staked_committee_size as u64,
10389                next_view_timeout_ms: v1_hotshot.next_view_timeout,
10390                view_sync_timeout_ms: v1_hotshot.view_sync_timeout.as_millis() as u64,
10391                builder_timeout_ms: v1_hotshot.builder_timeout.as_millis() as u64,
10392                data_request_delay_ms: v1_hotshot.data_request_delay.as_millis() as u64,
10393                builder_urls: v1_hotshot
10394                    .builder_urls
10395                    .iter()
10396                    .map(ToString::to_string)
10397                    .collect(),
10398                start_proposing_view: v1_hotshot.start_proposing_view,
10399                stop_proposing_view: v1_hotshot.stop_proposing_view,
10400                start_voting_view: v1_hotshot.start_voting_view,
10401                stop_voting_view: v1_hotshot.stop_voting_view,
10402                start_proposing_time: v1_hotshot.start_proposing_time,
10403                stop_proposing_time: v1_hotshot.stop_proposing_time,
10404                start_voting_time: v1_hotshot.start_voting_time,
10405                stop_voting_time: v1_hotshot.stop_voting_time,
10406                epoch_height: v1_hotshot.epoch_height,
10407                epoch_start_block: v1_hotshot.epoch_start_block,
10408                stake_table_capacity: v1_hotshot.stake_table_capacity as u64,
10409                drb_difficulty: v1_hotshot.drb_difficulty,
10410                drb_upgrade_difficulty: v1_hotshot.drb_upgrade_difficulty,
10411                known_nodes_with_stake: v1_hotshot
10412                    .known_nodes_with_stake
10413                    .iter()
10414                    .cloned()
10415                    .map(Into::into)
10416                    .collect(),
10417                known_da_nodes: v1_hotshot
10418                    .known_da_nodes
10419                    .iter()
10420                    .cloned()
10421                    .map(Into::into)
10422                    .collect(),
10423                da_committees: v1_hotshot
10424                    .da_committees
10425                    .iter()
10426                    .map(|da_committee| espresso_api::proto::VersionedDaCommittee {
10427                        start_version: da_committee.start_version.to_string(),
10428                        start_epoch: da_committee.start_epoch,
10429                        committee: da_committee
10430                            .committee
10431                            .iter()
10432                            .cloned()
10433                            .map(Into::into)
10434                            .collect(),
10435                    })
10436                    .collect(),
10437                fixed_leader_for_gpuvid: v1_hotshot.fixed_leader_for_gpuvid as u64,
10438                num_bootstrap: v1_hotshot.num_bootstrap as u64,
10439                commit_sha: v1_config.commit_sha().to_string(),
10440                indexed_da: v1_config.indexed_da(),
10441                builder: match v1_config.builder() {
10442                    hotshot_types::network::BuilderType::External => {
10443                        espresso_api::proto::BuilderType::External
10444                    },
10445                    hotshot_types::network::BuilderType::Simple => {
10446                        espresso_api::proto::BuilderType::Simple
10447                    },
10448                    hotshot_types::network::BuilderType::Random => {
10449                        espresso_api::proto::BuilderType::Random
10450                    },
10451                }
10452                .into(),
10453                ..Default::default()
10454            }
10455        );
10456        // The comparison above builds its expected value with the same `From<PeerConfig>` the
10457        // handler uses, so it would agree with itself. Check one peer against the v1 types.
10458        assert!(!v2_hotshot.known_nodes_with_stake.is_empty());
10459        assert!(!v2_hotshot.known_da_nodes.is_empty());
10460        let v1_peer = &v1_hotshot.known_nodes_with_stake[0];
10461        let v2_peer = &v2_hotshot.known_nodes_with_stake[0];
10462        let v2_entry = v2_peer.stake_table_entry.as_ref().unwrap();
10463        assert_eq!(
10464            v2_entry.stake_key.as_ref().unwrap().key,
10465            v1_peer.stake_table_entry.stake_key.to_string()
10466        );
10467        assert_eq!(
10468            v2_entry.stake_amount,
10469            format!("{:#x}", v1_peer.stake_table_entry.stake_amount)
10470        );
10471        assert_eq!(
10472            v2_peer.state_ver_key.as_ref().unwrap().key,
10473            v1_peer.state_ver_key.to_string()
10474        );
10475        assert_eq!(
10476            v2_peer.connect_info.as_ref().unwrap().p2p_addr,
10477            v1_peer
10478                .connect_info
10479                .as_ref()
10480                .unwrap()
10481                .p2p_addr
10482                .unbracketed_string()
10483        );
10484
10485        let v1_env: Vec<String> = client.get("config/env").send().await.unwrap();
10486        let v2_env: espresso_api::proto::EnvResponse =
10487            client.get("v2/config/env").send().await.unwrap();
10488        assert_eq!(
10489            v2_env
10490                .variables
10491                .iter()
10492                .map(|var| format!("{}={}", var.name, var.value))
10493                .collect::<Vec<_>>(),
10494            v1_env
10495        );
10496
10497        // A TestNetwork registers no runtime config, which v1 reports as 404; v2 must not turn
10498        // that into a 500.
10499        let err = client
10500            .get::<serde_json::Value>("v2/config/runtime")
10501            .send()
10502            .await
10503            .unwrap_err();
10504        assert_eq!(err.status, StatusCode::NOT_FOUND);
10505
10506        let v1_tables: Vec<TableSize> = client.get("database/table-sizes").send().await.unwrap();
10507        let v2_tables: espresso_api::proto::TableSizesResponse =
10508            client.get("v2/database/table-sizes").send().await.unwrap();
10509        // Names are stable; row counts and byte sizes are not, since the network keeps deciding
10510        // blocks between the two requests and Postgres reports both approximately.
10511        let sorted = |mut names: Vec<String>| {
10512            names.sort();
10513            names
10514        };
10515        assert_eq!(
10516            sorted(
10517                v2_tables
10518                    .tables
10519                    .iter()
10520                    .map(|t| t.table_name.clone())
10521                    .collect()
10522            ),
10523            sorted(v1_tables.iter().map(|t| t.table_name.clone()).collect())
10524        );
10525        // Postgres reports names schema-qualified (`hotshot.header`), SQLite bare.
10526        assert!(
10527            v2_tables
10528                .tables
10529                .iter()
10530                .any(|table| table.table_name.ends_with("header")),
10531            "{v2_tables:?}"
10532        );
10533        // Values drift like the counts above, so compare only whether each table reports a size.
10534        // That presence is the one thing the mapping could quietly change.
10535        let v1_sizes: HashMap<&str, bool> = v1_tables
10536            .iter()
10537            .map(|table| (table.table_name.as_str(), table.total_size_bytes.is_some()))
10538            .collect();
10539        for table in &v2_tables.tables {
10540            assert_eq!(
10541                v1_sizes.get(table.table_name.as_str()),
10542                Some(&table.total_size_bytes.is_some()),
10543                "{table:?}"
10544            );
10545        }
10546
10547        // Nothing in the repository runs a deferred migration yet, so without these rows both
10548        // versions report an empty list and agree vacuously.
10549        {
10550            let db = D::connect(&D::tmp_options(&storage[0]), StorageConnectionType::Query)
10551                .await
10552                .unwrap();
10553            for (name, started_at, completed_at, last_offset) in [
10554                (
10555                    "backfill_done",
10556                    "2026-01-02T03:04:05.123456Z",
10557                    Some("2026-01-02T03:14:15Z"),
10558                    Some(4242i64),
10559                ),
10560                ("backfill_running", "2026-01-02T03:24:25.5Z", None, Some(0)),
10561            ] {
10562                let parse = |time: &str| {
10563                    chrono::DateTime::parse_from_rfc3339(time)
10564                        .unwrap()
10565                        .with_timezone(&chrono::Utc)
10566                };
10567                let migration = DeferredMigration {
10568                    name,
10569                    started_at: parse(started_at),
10570                    completed_at: completed_at.map(parse),
10571                    last_offset,
10572                };
10573                D::insert_deferred_migration(&db, migration).await.unwrap();
10574            }
10575        }
10576
10577        // As raw JSON, so the timestamp strings are compared as v1 actually serves them.
10578        let v1_migrations: serde_json::Value = client
10579            .get("database/migration-status")
10580            .send()
10581            .await
10582            .unwrap();
10583        let v2_migrations: espresso_api::proto::MigrationStatusResponse = client
10584            .get("v2/database/migration-status")
10585            .send()
10586            .await
10587            .unwrap();
10588        let v1_migrations = v1_migrations.as_array().unwrap();
10589        assert_eq!(v1_migrations.len(), 2, "{v1_migrations:?}");
10590        assert_eq!(v2_migrations.migrations.len(), v1_migrations.len());
10591        for (v1, v2) in v1_migrations.iter().zip(&v2_migrations.migrations) {
10592            assert_eq!(v2.name, v1["name"].as_str().unwrap());
10593            assert_eq!(v2.started_at, v1["started_at"].as_str().unwrap());
10594            assert_eq!(
10595                v2.completed_at.as_deref(),
10596                v1["completed_at"].as_str(),
10597                "{v1:?}"
10598            );
10599            assert_eq!(v2.last_offset, v1["last_offset"].as_i64());
10600        }
10601
10602        check_availability_v2_parity(&client, port, first_block, last_block).await;
10603        check_merklized_state_v2_parity(&client, last_block).await;
10604    }
10605
10606    /// Every merklized-state response on v2 must be the conversion of what v1 serves for the same
10607    /// snapshot. The state is persisted behind decide, so this first waits for it to cover
10608    /// `last_block`, after which every snapshot below the state height is stable.
10609    async fn check_merklized_state_v2_parity(client: &HttpClient, last_block: u64) {
10610        use espresso_api::proto;
10611
10612        let state_height = tokio::time::timeout(Duration::from_secs(60), async {
10613            loop {
10614                let height: u64 = fetch(client, "block-state/block-height").await;
10615                if height > last_block {
10616                    return height;
10617                }
10618                sleep(Duration::from_millis(200)).await;
10619            }
10620        })
10621        .await
10622        .expect("merklized state never caught up");
10623        let v2: proto::StateHeightResponse = fetch(client, "v2/merklized-state/height").await;
10624        assert_eq!(v2.height, state_height);
10625
10626        // A snapshot at `state_height` commits to the headers below it, so the newest one is a
10627        // member and gets a leaf-first path.
10628        let key = state_height - 1;
10629        let v1_block: MerkleProof<Commitment<Header>, u64, Sha3Node, 3> =
10630            fetch(client, &format!("block-state/{state_height}/{key}")).await;
10631        let expected = proto::MerklePathResponse::from(&v1_block);
10632        let v2: proto::MerklePathResponse = fetch(
10633            client,
10634            &format!("v2/merklized-state/block/path?key={key}&height={state_height}"),
10635        )
10636        .await;
10637        assert_eq!(v2, expected);
10638        let root: Header = fetch(client, &format!("availability/header/{state_height}")).await;
10639        let commit = root.block_merkle_tree_root();
10640        let v1_block: MerkleProof<Commitment<Header>, u64, Sha3Node, 3> =
10641            fetch(client, &format!("block-state/commit/{commit}/{key}")).await;
10642        let v2: proto::MerklePathResponse = fetch(
10643            client,
10644            &format!("v2/merklized-state/block/path?key={key}&commit={commit}"),
10645        )
10646        .await;
10647        assert_eq!(v2, proto::MerklePathResponse::from(&v1_block));
10648
10649        // 0.6 charges no fees, but every test node prefunds the builder account at genesis, so it
10650        // is in the fee tree.
10651        let account = TestConfig::<5>::builder_key().fee_account();
10652        let v1_fee: MerkleProof<FeeAmount, FeeAccount, Sha3Node, 256> =
10653            fetch(client, &format!("fee-state/{state_height}/{account}")).await;
10654        let v2: proto::MerklePathResponse = fetch(
10655            client,
10656            &format!("v2/merklized-state/fee/path?address={account}&height={state_height}"),
10657        )
10658        .await;
10659        assert_eq!(v2, proto::MerklePathResponse::from(&v1_fee));
10660
10661        let v1_balance: Option<FeeAmount> =
10662            fetch(client, &format!("fee-state/fee-balance/latest/{account}")).await;
10663        let v2: proto::FeeBalanceResponse = fetch(
10664            client,
10665            &format!("v2/merklized-state/fee/balance?address={account}"),
10666        )
10667        .await;
10668        assert_eq!(
10669            v2.balance,
10670            v1_balance.expect("the builder has a balance").0.to_string()
10671        );
10672        // An account the tree has never seen is a zero balance, not an error.
10673        let unknown = FeeAccount::from(alloy::primitives::Address::repeat_byte(0xee));
10674        let v1_balance: Option<FeeAmount> =
10675            fetch(client, &format!("fee-state/fee-balance/latest/{unknown}")).await;
10676        assert!(v1_balance.is_none());
10677        let v2: proto::FeeBalanceResponse = fetch(
10678            client,
10679            &format!("v2/merklized-state/fee/balance?address={unknown}"),
10680        )
10681        .await;
10682        assert_eq!(v2.balance, "0");
10683
10684        let beyond = state_height + 1_000;
10685        for (v1, v2) in [
10686            (
10687                format!("block-state/{beyond}/{key}"),
10688                format!("v2/merklized-state/block/path?key={key}&height={beyond}"),
10689            ),
10690            (
10691                format!("block-state/commit/not-a-commitment/{key}"),
10692                format!("v2/merklized-state/block/path?key={key}&commit=not-a-commitment"),
10693            ),
10694            (
10695                format!("fee-state/{state_height}/not-an-address"),
10696                format!("v2/merklized-state/fee/path?address=not-an-address&height={state_height}"),
10697            ),
10698        ] {
10699            assert_eq!(
10700                error_status(client, &v2).await,
10701                error_status(client, &v1).await,
10702                "{v2}"
10703            );
10704        }
10705        for missing_selector in [
10706            format!("block/path?key={key}"),
10707            format!("block/path?key={key}&height={state_height}&commit={commit}"),
10708            format!("block/path?height={state_height}"),
10709            "fee/balance".to_owned(),
10710        ] {
10711            let status =
10712                error_status(client, &format!("v2/merklized-state/{missing_selector}")).await;
10713            assert_eq!(status, StatusCode::BAD_REQUEST, "{missing_selector}");
10714        }
10715    }
10716
10717    /// Every availability response on v2 must be the conversion of what v1 serves for the same
10718    /// request, with v2's query parameters selecting what v1's path segments do.
10719    async fn check_availability_v2_parity(
10720        client: &HttpClient,
10721        port: u16,
10722        first_block: u64,
10723        last_block: u64,
10724    ) {
10725        use espresso_api::proto;
10726        use espresso_types::{Header, NamespaceProofQueryData};
10727        use hotshot_query_service::availability::{
10728            BlockQueryData, BlockSummaryQueryData, LeafQueryData, Limits, PayloadQueryData,
10729            TransactionQueryData, TransactionWithProofQueryData, VidCommonQueryData,
10730        };
10731
10732        let v1_limits: Limits = fetch(client, "availability/limits").await;
10733        let limits: proto::LimitsResponse = fetch(client, "v2/availability/limits").await;
10734        assert_eq!(
10735            limits.small_object_range_limit,
10736            v1_limits.small_object_range_limit as u64
10737        );
10738        assert_eq!(
10739            limits.large_object_range_limit,
10740            v1_limits.large_object_range_limit as u64
10741        );
10742        assert!(limits.namespace_proof_range_limit > 0);
10743
10744        // A payload hash can match several blocks, so v1's answer for it is the reference rather
10745        // than block 1.
10746        let v1_header: Header = fetch(client, "availability/header/1").await;
10747        let header = proto::HeaderResponse::from(&v1_header);
10748        let payload_hash = v1_header.payload_commitment();
10749        let by_payload_hash: Header = fetch(
10750            client,
10751            &format!("availability/header/payload-hash/{payload_hash}"),
10752        )
10753        .await;
10754        for (query, expected) in [
10755            ("height=1".to_string(), header.clone()),
10756            (format!("hash={}", v1_header.commit()), header.clone()),
10757            (
10758                format!("payloadHash={payload_hash}"),
10759                proto::HeaderResponse::from(&by_payload_hash),
10760            ),
10761        ] {
10762            let v2: proto::HeaderResponse =
10763                fetch(client, &format!("v2/availability/header?{query}")).await;
10764            assert_eq!(v2, expected, "{query}");
10765        }
10766
10767        let v1_leaf: LeafQueryData<SeqTypes> = fetch(client, "availability/leaf/1").await;
10768        let leaf = proto::LeafResponse::from(&v1_leaf);
10769        for query in ["height=1".to_string(), format!("hash={}", v1_leaf.hash())] {
10770            let v2: proto::LeafResponse =
10771                fetch(client, &format!("v2/availability/leaf?{query}")).await;
10772            assert_eq!(v2, leaf, "{query}");
10773        }
10774
10775        let v1_block: BlockQueryData<SeqTypes> = fetch(client, "availability/block/1").await;
10776        let block = proto::BlockResponse::from(&v1_block);
10777        let block_hash = v1_block.hash();
10778        let by_payload_hash: BlockQueryData<SeqTypes> = fetch(
10779            client,
10780            &format!("availability/block/payload-hash/{payload_hash}"),
10781        )
10782        .await;
10783        for (query, expected) in [
10784            ("height=1".to_string(), block.clone()),
10785            (format!("hash={block_hash}"), block.clone()),
10786            (
10787                format!("payloadHash={payload_hash}"),
10788                proto::BlockResponse::from(&by_payload_hash),
10789            ),
10790        ] {
10791            let v2: proto::BlockResponse =
10792                fetch(client, &format!("v2/availability/block?{query}")).await;
10793            assert_eq!(v2, expected, "{query}");
10794        }
10795
10796        let v1_payload: PayloadQueryData<SeqTypes> = fetch(client, "availability/payload/1").await;
10797        let payload = proto::PayloadResponse::from(&v1_payload);
10798        let by_hash: PayloadQueryData<SeqTypes> =
10799            fetch(client, &format!("availability/payload/hash/{payload_hash}")).await;
10800        for (query, expected) in [
10801            ("height=1".to_string(), payload.clone()),
10802            (format!("blockHash={block_hash}"), payload.clone()),
10803            (
10804                format!("hash={payload_hash}"),
10805                proto::PayloadResponse::from(&by_hash),
10806            ),
10807        ] {
10808            let v2: proto::PayloadResponse =
10809                fetch(client, &format!("v2/availability/payload?{query}")).await;
10810            assert_eq!(v2, expected, "{query}");
10811        }
10812
10813        let v1_vid: VidCommonQueryData<SeqTypes> = fetch(client, "availability/vid/common/1").await;
10814        let vid = proto::VidCommonResponse::try_from(&v1_vid).unwrap();
10815        let by_payload_hash: VidCommonQueryData<SeqTypes> = fetch(
10816            client,
10817            &format!("availability/vid/common/payload-hash/{payload_hash}"),
10818        )
10819        .await;
10820        for (query, expected) in [
10821            ("height=1".to_string(), vid.clone()),
10822            (format!("hash={block_hash}"), vid.clone()),
10823            (
10824                format!("payloadHash={payload_hash}"),
10825                proto::VidCommonResponse::try_from(&by_payload_hash).unwrap(),
10826            ),
10827        ] {
10828            let v2: proto::VidCommonResponse =
10829                fetch(client, &format!("v2/availability/vid-common?{query}")).await;
10830            assert_eq!(v2, expected, "{query}");
10831        }
10832
10833        // Transactions are submitted only after connecting, so block 1 is empty and `last_block` is
10834        // the one height known to carry a transaction.
10835        for height in [1, last_block] {
10836            let v1: BlockSummaryQueryData<SeqTypes> =
10837                fetch(client, &format!("availability/block/summary/{height}")).await;
10838            let v2: proto::BlockSummaryResponse = fetch(
10839                client,
10840                &format!("v2/availability/block-summary?height={height}"),
10841            )
10842            .await;
10843            assert_eq!(v2, proto::BlockSummaryResponse::from(&v1), "{height}");
10844        }
10845
10846        let v1_tx: TransactionQueryData<SeqTypes> = fetch(
10847            client,
10848            &format!("availability/transaction/{last_block}/0/noproof"),
10849        )
10850        .await;
10851        let tx = proto::TransactionResponse::from(&v1_tx);
10852        let v1_proven: TransactionWithProofQueryData<SeqTypes> = fetch(
10853            client,
10854            &format!("availability/transaction/{last_block}/0/proof"),
10855        )
10856        .await;
10857        let proven = proto::TransactionWithProofResponse::try_from(&v1_proven).unwrap();
10858        for query in [
10859            format!("height={last_block}&index=0"),
10860            format!("hash={}", v1_tx.hash()),
10861        ] {
10862            let v2: proto::TransactionResponse =
10863                fetch(client, &format!("v2/availability/transaction?{query}")).await;
10864            assert_eq!(v2, tx, "{query}");
10865            let v2: proto::TransactionWithProofResponse = fetch(
10866                client,
10867                &format!("v2/availability/transaction-proof?{query}"),
10868            )
10869            .await;
10870            assert_eq!(v2, proven, "{query}");
10871        }
10872        // A 0.6 block is disseminated with AvidmGf2, so the proof must land on that arm and carry
10873        // the namespace proof of a non-empty transaction.
10874        let Some(proto::tx_proof::Proof::V2(v2)) = proven.proof.unwrap().proof else {
10875            panic!("a 0.6 block's inclusion proof is AvidmGf2");
10876        };
10877        assert!(v2.ns_proof.is_some());
10878
10879        // 102 is the namespace of the last submitted transaction, so `last_block` carries it.
10880        let v1_proof: NamespaceProofQueryData = fetch(
10881            client,
10882            &format!("availability/block/{last_block}/namespace/102"),
10883        )
10884        .await;
10885        assert!(v1_proof.proof.is_some() && !v1_proof.transactions.is_empty());
10886        let proof = proto::NamespaceProofResponse::try_from(&v1_proof).unwrap();
10887        let last: BlockQueryData<SeqTypes> =
10888            fetch(client, &format!("availability/block/{last_block}")).await;
10889        for selector in [
10890            format!("height={last_block}"),
10891            format!("hash={}", last.hash()),
10892            format!("payloadHash={}", last.payload_hash()),
10893        ] {
10894            let v2: proto::NamespaceProofResponse = fetch(
10895                client,
10896                &format!("v2/availability/namespace-proof?{selector}&namespace=102"),
10897            )
10898            .await;
10899            assert_eq!(v2, proof, "{selector}");
10900        }
10901        // A namespace no block carries is an absent proof, not an error.
10902        let v1_absent: NamespaceProofQueryData =
10903            fetch(client, "availability/block/1/namespace/4294967295").await;
10904        assert!(v1_absent.proof.is_none() && v1_absent.transactions.is_empty());
10905        let v2_absent: proto::NamespaceProofResponse = fetch(
10906            client,
10907            "v2/availability/namespace-proof?height=1&namespace=4294967295",
10908        )
10909        .await;
10910        assert_eq!(
10911            v2_absent,
10912            proto::NamespaceProofResponse::try_from(&v1_absent).unwrap()
10913        );
10914
10915        for missing_selector in [
10916            "header",
10917            "header?height=1&hash=x",
10918            "leaf",
10919            "block",
10920            "vid-common",
10921            "transaction?height=1",
10922            "namespace-proof?namespace=1",
10923            "block-range?from=0",
10924            // Without `from`, a stream would replay the chain from genesis.
10925            "stream/headers",
10926        ] {
10927            let status = error_status(client, &format!("v2/availability/{missing_selector}")).await;
10928            assert_eq!(status, StatusCode::BAD_REQUEST, "{missing_selector}");
10929        }
10930
10931        for route in [
10932            "availability/state-cert/1000000",
10933            "v2/availability/state-cert?epoch=1000000",
10934            "availability/state-cert-v2/1000000",
10935            "v2/availability/state-cert-v2?epoch=1000000",
10936        ] {
10937            assert_eq!(
10938                error_status(client, route).await,
10939                StatusCode::NOT_FOUND,
10940                "{route}"
10941            );
10942        }
10943
10944        // No cert2 exists for a height the network never reaches, and a block this network built
10945        // has no bad encoding to prove. These only show both versions refusing alike. The
10946        // conversions are covered by the reference-vector tests.
10947        for (v1, v2) in [
10948            (
10949                format!("availability/incorrect-encoding-proof/{last_block}/102"),
10950                format!(
10951                    "v2/availability/incorrect-encoding-proof?height={last_block}&namespace=102"
10952                ),
10953            ),
10954            (
10955                "availability/cert2/1000000".to_string(),
10956                "v2/availability/cert2?height=1000000".to_string(),
10957            ),
10958        ] {
10959            assert_eq!(
10960                error_status(client, &v2).await,
10961                error_status(client, &v1).await,
10962                "{v2}"
10963            );
10964        }
10965
10966        check_ranges_v2_parity(client, &limits, first_block, last_block).await;
10967
10968        // Each stream's first frame is the unary answer for the height it starts from.
10969        assert_eq!(
10970            first_sse_frame::<proto::LeafResponse>(port, "leaves?from=1").await,
10971            leaf
10972        );
10973        assert_eq!(
10974            first_sse_frame::<proto::HeaderResponse>(port, "headers?from=1").await,
10975            header
10976        );
10977        assert_eq!(
10978            first_sse_frame::<proto::BlockResponse>(port, "blocks?from=1").await,
10979            block
10980        );
10981        assert_eq!(
10982            first_sse_frame::<proto::PayloadResponse>(port, "payloads?from=1").await,
10983            payload
10984        );
10985        assert_eq!(
10986            first_sse_frame::<proto::VidCommonResponse>(port, "vid-common?from=1").await,
10987            vid
10988        );
10989        let streamed: proto::TransactionResponse =
10990            first_sse_frame(port, &format!("transactions?from={last_block}")).await;
10991        assert_eq!(streamed, tx);
10992        let streamed: proto::NamespaceProofResponse = first_sse_frame(
10993            port,
10994            &format!("namespace-proofs?from={last_block}&namespace=102"),
10995        )
10996        .await;
10997        assert_eq!(streamed, proof);
10998        // From `first_block`, whose transaction is in namespace 101, the filter must skip ahead.
10999        let streamed: proto::TransactionResponse = first_sse_frame(
11000            port,
11001            &format!("transactions?from={first_block}&namespace=102"),
11002        )
11003        .await;
11004        let v1: TransactionQueryData<SeqTypes> = fetch(
11005            client,
11006            &format!("availability/transaction/hash/{}/noproof", streamed.hash),
11007        )
11008        .await;
11009        assert_eq!(v1.namespace().0, 102);
11010        assert_eq!(streamed, proto::TransactionResponse::from(&v1));
11011    }
11012
11013    /// The range and batch endpoints reuse the single lookups' item conversions, so what these
11014    /// pin is that the bounds select what v1's do, and that each is held to its own limit class.
11015    async fn check_ranges_v2_parity(
11016        client: &HttpClient,
11017        limits: &espresso_api::proto::LimitsResponse,
11018        first_block: u64,
11019        last_block: u64,
11020    ) {
11021        use espresso_api::proto;
11022        use hotshot_query_service::availability::{
11023            BlockQueryData, BlockSummaryQueryData, LeafQueryData, PayloadQueryData,
11024            VidCommonQueryData,
11025        };
11026
11027        let (from, until) = (first_block, last_block + 1);
11028        let bounds = format!("{from}/{until}");
11029        let query = format!("from={from}&until={until}");
11030
11031        let headers: Vec<espresso_types::Header> =
11032            fetch(client, &format!("availability/header/{bounds}")).await;
11033        let v2_headers: proto::HeaderRangeResponse =
11034            fetch(client, &format!("v2/availability/header-range?{query}")).await;
11035        assert_eq!(v2_headers, proto::HeaderRangeResponse::from(&*headers));
11036        let leaves: Vec<LeafQueryData<SeqTypes>> =
11037            fetch(client, &format!("availability/leaf/{bounds}")).await;
11038        let v2_leaves: proto::LeafRangeResponse =
11039            fetch(client, &format!("v2/availability/leaf-range?{query}")).await;
11040        assert_eq!(v2_leaves, proto::LeafRangeResponse::from(&*leaves));
11041        let blocks: Vec<BlockQueryData<SeqTypes>> =
11042            fetch(client, &format!("availability/block/{bounds}")).await;
11043        let v2_blocks: proto::BlockRangeResponse =
11044            fetch(client, &format!("v2/availability/block-range?{query}")).await;
11045        assert_eq!(v2_blocks, proto::BlockRangeResponse::from(&*blocks));
11046        let payloads: Vec<PayloadQueryData<SeqTypes>> =
11047            fetch(client, &format!("availability/payload/{bounds}")).await;
11048        let v2_payloads: proto::PayloadRangeResponse =
11049            fetch(client, &format!("v2/availability/payload-range?{query}")).await;
11050        assert_eq!(v2_payloads, proto::PayloadRangeResponse::from(&*payloads));
11051        let vid: Vec<VidCommonQueryData<SeqTypes>> =
11052            fetch(client, &format!("availability/vid/common/{bounds}")).await;
11053        let v2_vid: proto::VidCommonRangeResponse =
11054            fetch(client, &format!("v2/availability/vid-common-range?{query}")).await;
11055        assert_eq!(
11056            v2_vid,
11057            proto::VidCommonRangeResponse::try_from(&*vid).unwrap()
11058        );
11059        let summaries: Vec<BlockSummaryQueryData<SeqTypes>> =
11060            fetch(client, &format!("availability/block/summaries/{bounds}")).await;
11061        let v2_summaries: proto::BlockSummaryRangeResponse = fetch(
11062            client,
11063            &format!("v2/availability/block-summary-range?{query}"),
11064        )
11065        .await;
11066        assert_eq!(
11067            v2_summaries,
11068            proto::BlockSummaryRangeResponse::from(&*summaries)
11069        );
11070        let proofs: Vec<espresso_types::NamespaceProofQueryData> = fetch(
11071            client,
11072            &format!("availability/block/{bounds}/namespace/102"),
11073        )
11074        .await;
11075        let v2_proofs: proto::NamespaceProofRangeResponse = fetch(
11076            client,
11077            &format!("v2/availability/namespace-proof-range?{query}&namespace=102"),
11078        )
11079        .await;
11080        assert_eq!(
11081            v2_proofs,
11082            proto::NamespaceProofRangeResponse::try_from(&*proofs).unwrap()
11083        );
11084
11085        // One height past each class's limit, so an endpoint held to the wrong class would pass
11086        // where v1 refuses.
11087        let small = limits.small_object_range_limit + 1;
11088        let large = limits.large_object_range_limit + 1;
11089        let namespace = limits.namespace_proof_range_limit + 1;
11090        for (v1, v2) in [
11091            (
11092                format!("availability/leaf/0/{small}"),
11093                format!("v2/availability/leaf-range?from=0&until={small}"),
11094            ),
11095            (
11096                format!("availability/vid/common/0/{small}"),
11097                format!("v2/availability/vid-common-range?from=0&until={small}"),
11098            ),
11099            (
11100                format!("availability/header/0/{large}"),
11101                format!("v2/availability/header-range?from=0&until={large}"),
11102            ),
11103            (
11104                format!("availability/block/0/{large}"),
11105                format!("v2/availability/block-range?from=0&until={large}"),
11106            ),
11107            (
11108                format!("availability/payload/0/{large}"),
11109                format!("v2/availability/payload-range?from=0&until={large}"),
11110            ),
11111            (
11112                format!("availability/block/summaries/0/{large}"),
11113                format!("v2/availability/block-summary-range?from=0&until={large}"),
11114            ),
11115            (
11116                format!("availability/block/0/{namespace}/namespace/102"),
11117                format!(
11118                    "v2/availability/namespace-proof-range?from=0&until={namespace}&namespace=102"
11119                ),
11120            ),
11121        ] {
11122            let v1_status = error_status(client, &v1).await;
11123            let v2_status = error_status(client, &v2).await;
11124            assert_eq!(v2_status, StatusCode::BAD_REQUEST, "{v2}");
11125            assert_eq!(v2_status, v1_status, "{v2}");
11126        }
11127
11128        // Two ranges with a gap between them, which is the case the batch endpoints exist for.
11129        let ranges = [first_block..first_block + 1, last_block..last_block + 1];
11130        let body = serde_json::json!({
11131            "ranges": ranges
11132                .iter()
11133                .map(|range| serde_json::json!({"from": range.start, "until": range.end}))
11134                .collect::<Vec<_>>(),
11135        });
11136        let leaves: Vec<LeafQueryData<SeqTypes>> =
11137            post(client, "availability/leaf/ranges", &ranges).await;
11138        assert_eq!(leaves.len(), 2);
11139        let v2_leaves: proto::LeafRangeResponse =
11140            post(client, "v2/availability/leaf-ranges", &body).await;
11141        assert_eq!(v2_leaves, proto::LeafRangeResponse::from(&*leaves));
11142        let blocks: Vec<BlockQueryData<SeqTypes>> =
11143            post(client, "availability/block/ranges", &ranges).await;
11144        let v2_blocks: proto::BlockRangeResponse =
11145            post(client, "v2/availability/block-ranges", &body).await;
11146        assert_eq!(v2_blocks, proto::BlockRangeResponse::from(&*blocks));
11147        let vid: Vec<VidCommonQueryData<SeqTypes>> =
11148            post(client, "availability/vid/common/ranges", &ranges).await;
11149        let v2_vid: proto::VidCommonRangeResponse =
11150            post(client, "v2/availability/vid-common-ranges", &body).await;
11151        assert_eq!(
11152            v2_vid,
11153            proto::VidCommonRangeResponse::try_from(&*vid).unwrap()
11154        );
11155        // Out of order is refused by the shared validation, and an absent bound by v2's own.
11156        for body in [
11157            serde_json::json!({"ranges": [
11158                {"from": last_block, "until": last_block + 1},
11159                {"from": first_block, "until": first_block + 1},
11160            ]}),
11161            serde_json::json!({"ranges": [{"from": first_block}]}),
11162        ] {
11163            let err = client
11164                .post::<proto::BlockRangeResponse>("v2/availability/block-ranges")
11165                .body_json(&body)
11166                .unwrap()
11167                .send()
11168                .await
11169                .unwrap_err();
11170            assert_eq!(err.status, StatusCode::BAD_REQUEST, "{body}");
11171        }
11172    }
11173
11174    /// Every reward-state response on v2 must be the conversion of what v1 serves for the same
11175    /// request. `height` must be one the light client contract finalized, since only those carry
11176    /// stored proofs, and `address` an account in the reward tree.
11177    async fn check_reward_state_v2_parity(
11178        client: &HttpClient,
11179        height: u64,
11180        address: alloy::primitives::Address,
11181    ) {
11182        use espresso_api::proto;
11183
11184        let v1_balance: espresso_types::v0_3::RewardAmount = fetch(
11185            client,
11186            &format!("reward-state-v2/reward-balance/{height}/{address}"),
11187        )
11188        .await;
11189        let v2: proto::RewardBalanceResponse = fetch(
11190            client,
11191            &format!("v2/merklized-state/reward/balance?address={address}&height={height}"),
11192        )
11193        .await;
11194        assert_eq!(v2.balance, v1_balance.to_string());
11195        let v1_latest: espresso_types::v0_3::RewardAmount = fetch(
11196            client,
11197            &format!("reward-state-v2/reward-balance/latest/{address}"),
11198        )
11199        .await;
11200        let v2: proto::RewardBalanceResponse = fetch(
11201            client,
11202            &format!("v2/merklized-state/reward/balance?address={address}"),
11203        )
11204        .await;
11205        assert_eq!(v2.balance, v1_latest.to_string());
11206
11207        for (v1, v2) in [
11208            (
11209                format!("reward-state-v2/proof/{height}/{address}"),
11210                format!("v2/merklized-state/reward/proof?address={address}&height={height}"),
11211            ),
11212            (
11213                format!("reward-state-v2/proof/latest/{address}"),
11214                format!("v2/merklized-state/reward/proof?address={address}"),
11215            ),
11216        ] {
11217            let v1_proof: RewardAccountQueryDataV2 = fetch(client, &v1).await;
11218            assert!(matches!(
11219                v1_proof.proof.proof,
11220                RewardMerkleProofV2::Presence(_)
11221            ));
11222            let v2_proof: proto::RewardAccountProofResponse = fetch(client, &v2).await;
11223            assert_eq!(
11224                v2_proof,
11225                proto::RewardAccountProofResponse::from(v1_proof),
11226                "{v2}"
11227            );
11228        }
11229
11230        let v1_claim: RewardClaimInput = fetch(
11231            client,
11232            &format!("reward-state-v2/reward-claim-input/{height}/{address}"),
11233        )
11234        .await;
11235        let v2: proto::RewardClaimInputResponse = fetch(
11236            client,
11237            &format!("v2/merklized-state/reward/claim-input?address={address}&height={height}"),
11238        )
11239        .await;
11240        assert_eq!(v2.lifetime_rewards, v1_claim.lifetime_rewards.to_string());
11241        assert_eq!(
11242            v2.auth_data,
11243            alloy::primitives::Bytes::from(v1_claim.auth_data).to_string()
11244        );
11245
11246        // v1 reverses each page, and v2 serves the tree's own order.
11247        let v1_amounts: Vec<(
11248            alloy::primitives::Address,
11249            espresso_types::v0_3::RewardAmount,
11250        )> = fetch(
11251            client,
11252            &format!("reward-state-v2/reward-amounts/{height}/0/1000"),
11253        )
11254        .await;
11255        let v2: proto::RewardAmountsResponse = fetch(
11256            client,
11257            &format!("v2/merklized-state/reward/amounts?height={height}&offset=0&limit=1000"),
11258        )
11259        .await;
11260        assert!(!v1_amounts.is_empty());
11261        assert_eq!(
11262            v2.amounts,
11263            v1_amounts
11264                .iter()
11265                .rev()
11266                .map(|(address, amount)| proto::RewardAmountPair {
11267                    address: address.to_string(),
11268                    amount: amount.to_string(),
11269                })
11270                .collect::<Vec<_>>()
11271        );
11272
11273        let v1_tree: Vec<u8> = fetch(
11274            client,
11275            &format!("reward-state-v2/reward-merkle-tree-v2/{height}"),
11276        )
11277        .await;
11278        let v2: proto::RewardMerkleTreeV2Response = fetch(
11279            client,
11280            &format!("v2/merklized-state/reward/tree?height={height}"),
11281        )
11282        .await;
11283        assert_eq!(v2.tree, v1_tree);
11284
11285        let absent = alloy::primitives::Address::with_last_byte(0xaa);
11286        let beyond = height + 1_000_000;
11287        for (v1, v2) in [
11288            (
11289                format!("reward-state-v2/reward-balance/{height}/{absent}"),
11290                format!("v2/merklized-state/reward/balance?address={absent}&height={height}"),
11291            ),
11292            (
11293                format!("reward-state-v2/proof/{height}/{absent}"),
11294                format!("v2/merklized-state/reward/proof?address={absent}&height={height}"),
11295            ),
11296            (
11297                format!("reward-state-v2/reward-claim-input/{height}/{absent}"),
11298                format!("v2/merklized-state/reward/claim-input?address={absent}&height={height}"),
11299            ),
11300            (
11301                format!("reward-state-v2/reward-balance/{height}/not-an-address"),
11302                format!("v2/merklized-state/reward/balance?address=not-an-address&height={height}"),
11303            ),
11304            (
11305                format!("reward-state-v2/reward-balance/{beyond}/{address}"),
11306                format!("v2/merklized-state/reward/balance?address={address}&height={beyond}"),
11307            ),
11308            (
11309                format!("reward-state-v2/reward-amounts/{height}/0/10001"),
11310                format!("v2/merklized-state/reward/amounts?height={height}&offset=0&limit=10001"),
11311            ),
11312            (
11313                format!("reward-state-v2/reward-amounts/{height}/1000000/10"),
11314                format!(
11315                    "v2/merklized-state/reward/amounts?height={height}&offset=1000000&limit=10"
11316                ),
11317            ),
11318        ] {
11319            assert_eq!(
11320                error_status(client, &v2).await,
11321                error_status(client, &v1).await,
11322                "{v2}"
11323            );
11324        }
11325        for missing in [
11326            "balance".to_owned(),
11327            "proof".to_owned(),
11328            format!("claim-input?address={address}"),
11329            format!("claim-input?height={height}"),
11330            format!("amounts?height={height}&offset=0"),
11331            "tree".to_owned(),
11332        ] {
11333            let status =
11334                error_status(client, &format!("v2/merklized-state/reward/{missing}")).await;
11335            assert_eq!(status, StatusCode::BAD_REQUEST, "{missing}");
11336        }
11337    }
11338
11339    type HttpClient = Client<ClientErr, StaticVersion<0, 1>>;
11340
11341    async fn fetch<T>(client: &HttpClient, route: &str) -> T
11342    where
11343        T: serde::de::DeserializeOwned,
11344    {
11345        client
11346            .get(route)
11347            .send()
11348            .await
11349            .unwrap_or_else(|err| panic!("{route}: {err}"))
11350    }
11351
11352    async fn post<T, B>(client: &HttpClient, route: &str, body: &B) -> T
11353    where
11354        T: serde::de::DeserializeOwned,
11355        B: serde::Serialize,
11356    {
11357        client
11358            .post(route)
11359            .body_json(body)
11360            .unwrap()
11361            .send()
11362            .await
11363            .unwrap_or_else(|err| panic!("{route}: {err}"))
11364    }
11365
11366    async fn error_status(client: &HttpClient, route: &str) -> StatusCode {
11367        client
11368            .get::<serde_json::Value>(route)
11369            .send()
11370            .await
11371            .expect_err(route)
11372            .status
11373    }
11374
11375    /// The first data frame of a v2 availability stream, read under a deadline since a stream
11376    /// never ends on its own.
11377    async fn first_sse_frame<T>(port: u16, stream: &str) -> T
11378    where
11379        T: serde::de::DeserializeOwned,
11380    {
11381        let mut response = reqwest::Client::new()
11382            .get(format!(
11383                "http://localhost:{port}/v2/availability/stream/{stream}"
11384            ))
11385            .header("Accept", "text/event-stream")
11386            .send()
11387            .await
11388            .unwrap();
11389        assert!(
11390            response.headers()["content-type"]
11391                .to_str()
11392                .unwrap()
11393                .starts_with("text/event-stream"),
11394            "{stream}: {:?}",
11395            response.headers()
11396        );
11397        let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(60);
11398        let mut body = Vec::new();
11399        loop {
11400            let chunk = tokio::time::timeout_at(deadline, response.chunk())
11401                .await
11402                .unwrap_or_else(|_| panic!("{stream}: no event before the deadline"))
11403                .unwrap()
11404                .expect("stream still open");
11405            body.extend_from_slice(&chunk);
11406            // An event ends at a blank line and a chunk boundary can split one, even mid
11407            // character, so only the events before the last blank line are decoded. A keep-alive
11408            // comment is a complete event with no data line, which is skipped.
11409            let Some(end) = body.windows(2).rposition(|pair| pair == b"\n\n") else {
11410                continue;
11411            };
11412            let complete = std::str::from_utf8(&body[..end]).unwrap();
11413            if let Some(data) = complete
11414                .split("\n\n")
11415                .find_map(|event| event.lines().find_map(|line| line.strip_prefix("data:")))
11416            {
11417                return serde_json::from_str(data.trim())
11418                    .unwrap_or_else(|err| panic!("{stream}: {err}: {data}"));
11419            }
11420        }
11421    }
11422
11423    /// Asks both versions for each light-client proof of the block at `height`, where `namespace`
11424    /// has transactions, and holds every v2 response to the v1 proof re-encoded.
11425    async fn assert_v2_light_client_agrees_with_v1(
11426        client: &Client<ClientErr, StaticVersion<0, 1>>,
11427        height: u64,
11428        namespace: u64,
11429    ) {
11430        use espresso_api::proto;
11431
11432        let payloads_to_proto = |proofs: &[PayloadProof]| {
11433            proofs
11434                .iter()
11435                .map(|proof| proto::LightClientPayloadProofResponse::try_from(proof).unwrap())
11436                .collect::<Vec<_>>()
11437        };
11438
11439        let v1_leaf: LeafProof = client
11440            .get(&format!("light-client/leaf/{height}"))
11441            .send()
11442            .await
11443            .unwrap();
11444        let v2_leaf: proto::LightClientLeafProofResponse = client
11445            .get(&format!("v2/light-client/leaf?height={height}"))
11446            .send()
11447            .await
11448            .unwrap();
11449        assert_eq!(v2_leaf, (&v1_leaf).into());
11450
11451        let root = height + 1;
11452        let v1_assumed: LeafProof = client
11453            .get(&format!("light-client/leaf/{height}/{root}"))
11454            .send()
11455            .await
11456            .unwrap();
11457        let v2_assumed: proto::LightClientLeafProofResponse = client
11458            .get(&format!(
11459                "v2/light-client/leaf?height={height}&finalized={root}"
11460            ))
11461            .send()
11462            .await
11463            .unwrap();
11464        assert_eq!(v2_assumed, (&v1_assumed).into());
11465
11466        let v1_header: HeaderProof = client
11467            .get(&format!("light-client/header/{root}/{height}"))
11468            .send()
11469            .await
11470            .unwrap();
11471        let v2_header: proto::LightClientHeaderProofResponse = client
11472            .get(&format!(
11473                "v2/light-client/header?root={root}&height={height}"
11474            ))
11475            .send()
11476            .await
11477            .unwrap();
11478        assert_eq!(
11479            v2_header,
11480            proto::LightClientHeaderProofResponse {
11481                header: Some(v1_header.header().into()),
11482                proof: Some(v1_header.proof().into()),
11483            }
11484        );
11485
11486        let v1_payload: PayloadProof = client
11487            .get(&format!("light-client/payload/{height}"))
11488            .send()
11489            .await
11490            .unwrap();
11491        let v2_payload: proto::LightClientPayloadProofResponse = client
11492            .get(&format!("v2/light-client/payload?height={height}"))
11493            .send()
11494            .await
11495            .unwrap();
11496        assert_eq!(v2_payload, (&v1_payload).try_into().unwrap());
11497
11498        let v1_payloads: Vec<PayloadProof> = client
11499            .get(&format!("light-client/payload/{height}/{root}"))
11500            .send()
11501            .await
11502            .unwrap();
11503        let v2_payloads: proto::LightClientPayloadProofRangeResponse = client
11504            .get(&format!(
11505                "v2/light-client/payload-range?from={height}&until={root}"
11506            ))
11507            .send()
11508            .await
11509            .unwrap();
11510        assert_eq!(v2_payloads.proofs, payloads_to_proto(&v1_payloads));
11511
11512        let v1_batch: Vec<PayloadProof> = client
11513            .post("light-client/payload/ranges")
11514            .body_json(&std::iter::once(height..root).collect::<Vec<_>>())
11515            .unwrap()
11516            .send()
11517            .await
11518            .unwrap();
11519        let v2_batch: proto::LightClientPayloadProofRangeResponse = client
11520            .post("v2/light-client/payload-ranges")
11521            .body_json(&proto::GetLightClientPayloadProofRangesRequest {
11522                ranges: vec![proto::HeightRange {
11523                    from: Some(height),
11524                    until: Some(root),
11525                }],
11526            })
11527            .unwrap()
11528            .send()
11529            .await
11530            .unwrap();
11531        assert_eq!(v2_batch.proofs, payloads_to_proto(&v1_batch));
11532
11533        let absent = 999;
11534        for ns in [namespace, absent] {
11535            let v1_ns: NamespaceProof = client
11536                .get(&format!("light-client/namespace/{height}/{ns}"))
11537                .send()
11538                .await
11539                .unwrap();
11540            let v2_ns: proto::LightClientNamespaceProofResponse = client
11541                .get(&format!(
11542                    "v2/light-client/namespace?height={height}&namespace={ns}"
11543                ))
11544                .send()
11545                .await
11546                .unwrap();
11547            assert_eq!(v1_ns.contents().is_some(), ns == namespace, "{ns}");
11548            assert_eq!(v2_ns, (&v1_ns).try_into().unwrap(), "{ns}");
11549        }
11550
11551        let v1_range: Vec<NamespaceProof> = client
11552            .get(&format!(
11553                "light-client/namespace/{height}/{root}/{namespace}"
11554            ))
11555            .send()
11556            .await
11557            .unwrap();
11558        let v2_range: proto::LightClientNamespaceProofRangeResponse = client
11559            .get(&format!(
11560                "v2/light-client/namespace-range?from={height}&until={root}&namespace={namespace}"
11561            ))
11562            .send()
11563            .await
11564            .unwrap();
11565        assert_eq!(
11566            v2_range.proofs,
11567            v1_range
11568                .iter()
11569                .map(|proof| proto::LightClientNamespaceProofResponse::try_from(proof).unwrap())
11570                .collect::<Vec<_>>()
11571        );
11572
11573        let encoded = tagged_base64::TaggedBase64::new(
11574            ::light_client::client::NAMESPACES_PARAM_TAG,
11575            &serde_json::to_vec(&[namespace, absent]).unwrap(),
11576        )
11577        .unwrap();
11578        let v1_blocks: Vec<std::collections::HashMap<u64, NamespaceProof>> = client
11579            .get(&format!(
11580                "light-client/namespaces/{height}/{root}/{encoded}"
11581            ))
11582            .send()
11583            .await
11584            .unwrap();
11585        let v2_blocks: proto::LightClientNamespacesProofRangeResponse = client
11586            .post("v2/light-client/namespaces-range")
11587            .body_json(&proto::GetLightClientNamespacesProofRangeRequest {
11588                from: Some(height),
11589                until: Some(root),
11590                namespaces: Vec::from([namespace, absent]),
11591            })
11592            .unwrap()
11593            .send()
11594            .await
11595            .unwrap();
11596        assert_eq!(
11597            v2_blocks.blocks,
11598            v1_blocks
11599                .iter()
11600                .map(|block| proto::LightClientNamespacesProof {
11601                    proofs: block
11602                        .iter()
11603                        .filter_map(|(ns, proof)| {
11604                            Some((*ns, proof.contents()?.try_into().unwrap()))
11605                        })
11606                        .collect(),
11607                })
11608                .collect::<Vec<_>>(),
11609        );
11610
11611        for query in [
11612            format!("light-client/namespace/{}/{namespace}", u64::MAX),
11613            format!(
11614                "v2/light-client/namespace?height={}&namespace={namespace}",
11615                u64::MAX
11616            ),
11617        ] {
11618            let err = client
11619                .get::<serde_json::Value>(&query)
11620                .send()
11621                .await
11622                .unwrap_err();
11623            assert_eq!(err.status, StatusCode::NOT_FOUND, "{query}");
11624        }
11625
11626        for query in [
11627            "v2/light-client/leaf".to_owned(),
11628            format!("v2/light-client/leaf?height={height}&block_hash=BLOCK~AA"),
11629            format!("v2/light-client/header?height={height}"),
11630            format!("v2/light-client/header?root={root}"),
11631            "v2/light-client/stake-table".to_owned(),
11632            format!("v2/light-client/namespace?height={height}"),
11633            format!("v2/light-client/payload-range?from={height}"),
11634            format!("v2/light-client/payload-range?from={root}&until={height}"),
11635            format!("v2/light-client/namespace?height={height}&namespace=4294967296"),
11636        ] {
11637            let err = client
11638                .get::<serde_json::Value>(&query)
11639                .send()
11640                .await
11641                .unwrap_err();
11642            assert_eq!(err.status, StatusCode::BAD_REQUEST, "{query}");
11643        }
11644
11645        for namespaces in [(0..=100).collect::<Vec<u64>>(), vec![1 << 32]] {
11646            let err = client
11647                .post::<serde_json::Value>("v2/light-client/namespaces-range")
11648                .body_json(
11649                    &espresso_api::proto::GetLightClientNamespacesProofRangeRequest {
11650                        from: Some(height),
11651                        until: Some(root),
11652                        namespaces,
11653                    },
11654                )
11655                .unwrap()
11656                .send()
11657                .await
11658                .unwrap_err();
11659            assert_eq!(err.status, StatusCode::BAD_REQUEST);
11660        }
11661    }
11662
11663    use rand::thread_rng;
11664
11665    #[apply(sql_data_sources)]
11666    async fn test_aggregator_namespace_endpoints<D: TestableSqlDataSource>(_d: PhantomData<D>) {
11667        let mut rng = thread_rng();
11668
11669        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
11670
11671        let url = format!("http://localhost:{port}").parse().unwrap();
11672        tracing::info!("Sequencer URL = {url}");
11673        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
11674
11675        let options = Options::with_port(port).submit(Default::default());
11676        const NUM_NODES: usize = 2;
11677        // Initialize storage for each node
11678        let storage = join_all((0..NUM_NODES).map(|_| D::create_storage())).await;
11679
11680        let persistence_options: [_; NUM_NODES] = storage
11681            .iter()
11682            .map(<D as TestableSequencerDataSource>::persistence_options)
11683            .collect::<Vec<_>>()
11684            .try_into()
11685            .unwrap();
11686
11687        let network_config = TestConfigBuilder::default().build();
11688
11689        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
11690            .api_config(D::options(&storage[0], options))
11691            .network_config(network_config)
11692            .persistences(persistence_options.clone())
11693            .build();
11694        let network = TestNetwork::new(config, TEST_UPGRADE).await;
11695        let mut events = network.server.event_stream();
11696        let start = Instant::now();
11697        let mut total_transactions = 0;
11698        let mut tx_heights = Vec::new();
11699        let mut sizes = HashMap::new();
11700        // inserting transactions for some namespaces
11701        // the number of transactions inserted is equal to namespace number.
11702        for namespace in 1..=4 {
11703            for _count in 0..namespace {
11704                // Generate a random payload length between 4 and 10 bytes
11705                let payload_len = rng.gen_range(4..=10);
11706                let payload: Vec<u8> = (0..payload_len).map(|_| rng.r#gen()).collect();
11707
11708                let txn = Transaction::new(NamespaceId::from(namespace as u32), payload);
11709
11710                client.connect(None).await;
11711
11712                let hash = client
11713                    .post("submit/submit")
11714                    .body_json(&txn)
11715                    .unwrap()
11716                    .send()
11717                    .await
11718                    .unwrap();
11719                assert_eq!(txn.commit(), hash);
11720
11721                // Wait for a Decide event containing transaction matching the one we sent
11722                let (height, size) = wait_for_decide_on_handle(&mut events, &txn).await;
11723                tx_heights.push(height);
11724                total_transactions += 1;
11725                *sizes.entry(namespace).or_insert(0) += size;
11726            }
11727        }
11728
11729        let duration = start.elapsed();
11730
11731        println!("Time elapsed to submit transactions: {duration:?}");
11732
11733        let last_tx_height = tx_heights.last().unwrap();
11734
11735        // Decide events fire when consensus decides a block, but the aggregator that backs these
11736        // endpoints runs as a separate background task. Wait for it to have written rows up to
11737        // last_tx_height before asserting; otherwise queries can hit a not-yet-aggregated height
11738        // and 404.
11739        let aggregator_deadline = Instant::now() + Duration::from_secs(30);
11740        loop {
11741            let count = client
11742                .get::<u64>(&format!("node/transactions/count/{last_tx_height}"))
11743                .send()
11744                .await
11745                .ok();
11746            if count == Some(total_transactions) {
11747                break;
11748            }
11749            assert!(
11750                Instant::now() < aggregator_deadline,
11751                "aggregator did not catch up to height {last_tx_height} (got {count:?}, expected \
11752                 {total_transactions})"
11753            );
11754            sleep(Duration::from_secs(1)).await;
11755        }
11756
11757        for namespace in 1..=4 {
11758            let count = client
11759                .get::<u64>(&format!("node/transactions/count/namespace/{namespace}"))
11760                .send()
11761                .await
11762                .unwrap();
11763            assert_eq!(
11764                count, namespace as u64,
11765                "Incorrect transaction count for namespace {namespace}: expected {namespace}, got \
11766                 {count}"
11767            );
11768
11769            // check the range endpoint
11770            let to_endpoint_count = client
11771                .get::<u64>(&format!(
11772                    "node/transactions/count/namespace/{namespace}/{last_tx_height}"
11773                ))
11774                .send()
11775                .await
11776                .unwrap();
11777            assert_eq!(
11778                to_endpoint_count, namespace as u64,
11779                "Incorrect transaction count for range endpoint (to only) for namespace \
11780                 {namespace}: expected {namespace}, got {to_endpoint_count}"
11781            );
11782
11783            // check the range endpoint
11784            let from_to_endpoint_count = client
11785                .get::<u64>(&format!(
11786                    "node/transactions/count/namespace/{namespace}/0/{last_tx_height}"
11787                ))
11788                .send()
11789                .await
11790                .unwrap();
11791            assert_eq!(
11792                from_to_endpoint_count, namespace as u64,
11793                "Incorrect transaction count for range endpoint (from-to) for namespace \
11794                 {namespace}: expected {namespace}, got {from_to_endpoint_count}"
11795            );
11796
11797            let ns_size = client
11798                .get::<usize>(&format!("node/payloads/size/namespace/{namespace}"))
11799                .send()
11800                .await
11801                .unwrap();
11802
11803            let expected_ns_size = *sizes.get(&namespace).unwrap();
11804            assert_eq!(
11805                ns_size, expected_ns_size,
11806                "Incorrect payload size for namespace {namespace}: expected {expected_ns_size}, \
11807                 got {ns_size}"
11808            );
11809
11810            let ns_size_to = client
11811                .get::<usize>(&format!(
11812                    "node/payloads/size/namespace/{namespace}/{last_tx_height}"
11813                ))
11814                .send()
11815                .await
11816                .unwrap();
11817            assert_eq!(
11818                ns_size_to, expected_ns_size,
11819                "Incorrect payload size for namespace {namespace} up to height {last_tx_height}: \
11820                 expected {expected_ns_size}, got {ns_size_to}"
11821            );
11822
11823            let ns_size_from_to = client
11824                .get::<usize>(&format!(
11825                    "node/payloads/size/namespace/{namespace}/0/{last_tx_height}"
11826                ))
11827                .send()
11828                .await
11829                .unwrap();
11830            assert_eq!(
11831                ns_size_from_to, expected_ns_size,
11832                "Incorrect payload size for namespace {namespace} from 0 to height \
11833                 {last_tx_height}: expected {expected_ns_size}, got {ns_size_from_to}"
11834            );
11835        }
11836
11837        let total_tx_count = client
11838            .get::<u64>("node/transactions/count")
11839            .send()
11840            .await
11841            .unwrap();
11842        assert_eq!(
11843            total_tx_count, total_transactions,
11844            "Incorrect total transaction count: expected {total_transactions}, got \
11845             {total_tx_count}"
11846        );
11847
11848        let total_payload_size = client
11849            .get::<usize>("node/payloads/size")
11850            .send()
11851            .await
11852            .unwrap();
11853
11854        let expected_total_size: usize = sizes.values().copied().sum();
11855        assert_eq!(
11856            total_payload_size, expected_total_size,
11857            "Incorrect total payload size: expected {expected_total_size}, got \
11858             {total_payload_size}"
11859        );
11860    }
11861
11862    #[apply(sql_data_sources)]
11863    async fn test_stream_transactions_endpoint<D: TestableSqlDataSource>(_d: PhantomData<D>) {
11864        // This test submits transactions to a sequencer for multiple namespaces,
11865        // waits for them to be decided, and then verifies that:
11866        // 1. All transactions appear in the transaction stream.
11867        // 2. Each namespace-specific transaction stream only includes the transactions of that namespace.
11868
11869        let mut rng = thread_rng();
11870
11871        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
11872
11873        let url = format!("http://localhost:{port}").parse().unwrap();
11874        tracing::info!("Sequencer URL = {url}");
11875        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
11876
11877        let options = Options::with_port(port).submit(Default::default());
11878        const NUM_NODES: usize = 2;
11879        // Initialize storage for each node
11880        let storage = join_all((0..NUM_NODES).map(|_| D::create_storage())).await;
11881
11882        let persistence_options: [_; NUM_NODES] = storage
11883            .iter()
11884            .map(<D as TestableSequencerDataSource>::persistence_options)
11885            .collect::<Vec<_>>()
11886            .try_into()
11887            .unwrap();
11888
11889        let network_config = TestConfigBuilder::default().build();
11890
11891        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
11892            .api_config(D::options(&storage[0], options))
11893            .network_config(network_config)
11894            .persistences(persistence_options.clone())
11895            .build();
11896        let network = TestNetwork::new(config, TEST_UPGRADE).await;
11897        let mut events = network.server.event_stream();
11898        let mut all_transactions = HashMap::new();
11899        let mut namespace_tx: HashMap<_, HashSet<_>> = HashMap::new();
11900
11901        // Submit transactions to namespaces 1 through 4
11902
11903        for namespace in 1..=4 {
11904            for _count in 0..namespace {
11905                let payload_len = rng.gen_range(4..=10);
11906                let payload: Vec<u8> = (0..payload_len).map(|_| rng.r#gen()).collect();
11907
11908                let txn = Transaction::new(NamespaceId::from(namespace as u32), payload);
11909
11910                client.connect(None).await;
11911
11912                let hash = client
11913                    .post("submit/submit")
11914                    .body_json(&txn)
11915                    .unwrap()
11916                    .send()
11917                    .await
11918                    .unwrap();
11919                assert_eq!(txn.commit(), hash);
11920
11921                // Wait for a Decide event containing transaction matching the one we sent
11922                wait_for_decide_on_handle(&mut events, &txn).await;
11923                // Store transaction for later validation
11924
11925                all_transactions.insert(txn.commit(), txn.clone());
11926                namespace_tx.entry(namespace).or_default().insert(txn);
11927            }
11928        }
11929
11930        let mut transactions = client
11931            .socket("availability/stream/transactions/0")
11932            .subscribe::<TransactionQueryData<SeqTypes>>()
11933            .await
11934            .expect("failed to subscribe to transactions endpoint");
11935
11936        let mut count = 0;
11937        while let Some(tx) = transactions.next().await {
11938            let tx = tx.unwrap();
11939            let expected = all_transactions
11940                .get(&tx.transaction().commit())
11941                .expect("txn not found ");
11942            assert_eq!(tx.transaction(), expected, "invalid transaction");
11943            count += 1;
11944
11945            if count == all_transactions.len() {
11946                break;
11947            }
11948        }
11949
11950        // Validate namespace-specific stream endpoint
11951
11952        for (namespace, expected_ns_txns) in &namespace_tx {
11953            let mut api_namespace_txns = client
11954                .socket(&format!(
11955                    "availability/stream/transactions/0/namespace/{namespace}",
11956                ))
11957                .subscribe::<TransactionQueryData<SeqTypes>>()
11958                .await
11959                .unwrap_or_else(|_| {
11960                    panic!("failed to subscribe to transactions namespace {namespace}")
11961                });
11962
11963            let mut received = HashSet::new();
11964
11965            while let Some(res) = api_namespace_txns.next().await {
11966                let tx = res.expect("stream error");
11967                received.insert(tx.transaction().clone());
11968
11969                if received.len() == expected_ns_txns.len() {
11970                    break;
11971                }
11972            }
11973
11974            assert_eq!(
11975                received, *expected_ns_txns,
11976                "Mismatched transactions for namespace {namespace}"
11977            );
11978        }
11979    }
11980
11981    /// Rewards accrue in the v1 tree before 0.4 and in the v2 tree from 0.4 on. The replay checks
11982    /// every derived root against its header, so this only checks which tree each era filled. The
11983    /// v1 balances are not queryable: the state loop stores no v1 paths since #4325.
11984    #[rstest]
11985    #[case::v3("v3", false)]
11986    #[case::v4("v4", true)]
11987    #[test_log::test(tokio::test(flavor = "multi_thread"))]
11988    async fn test_legacy_reward_tree_updates<D: TestableSqlDataSource>(
11989        #[values(PhantomData::<Postgres>, PhantomData::<Sqlite>)] _d: PhantomData<D>,
11990
11991        #[case] chain: &str,
11992        #[case] fills_v2: bool,
11993    ) -> anyhow::Result<()> {
11994        let chain = LegacyChain::load(chain)?;
11995        let replay = chain.replay::<D>().await?;
11996        let client = &replay.client;
11997        let tip = chain.tip();
11998
11999        let roots: Vec<_> = chain
12000            .blocks
12001            .iter()
12002            .map(|block| block.leaf.header().reward_merkle_tree_root())
12003            .collect();
12004        ensure!(
12005            roots.iter().all(|root| root.is_right() == fills_v2),
12006            "{} headers commit to the wrong reward tree",
12007            chain.name
12008        );
12009        let reward_blocks = roots.windows(2).filter(|pair| pair[0] != pair[1]).count();
12010        ensure!(reward_blocks > 0, "{} never paid a reward", chain.name);
12011
12012        let validators: AuthenticatedValidatorMap = client.get("node/validators/3").send().await?;
12013        ensure!(!validators.is_empty(), "epoch 3 has validators");
12014        let mut balances = vec![];
12015        for validator in validators.keys() {
12016            let balance = client
12017                .get::<Option<RewardAmount>>(&format!(
12018                    "reward-state-v2/reward-balance/{tip}/{validator}"
12019                ))
12020                .send()
12021                .await;
12022            if fills_v2 {
12023                balances.push(balance?.unwrap_or_default().0);
12024            } else {
12025                // The v2 tree was never written, so there is no proof to answer with.
12026                match balance {
12027                    Err(err) if err.status == StatusCode::NOT_FOUND => {},
12028                    other => bail!(
12029                        "{}: {validator} v2 balance should be missing, got {other:?}",
12030                        chain.name
12031                    ),
12032                }
12033            }
12034        }
12035        ensure!(
12036            balances.iter().any(|balance| !balance.is_zero()) == fills_v2,
12037            "{}: v2 reward balances {balances:?}",
12038            chain.name
12039        );
12040        Ok(())
12041    }
12042
12043    #[apply(sql_data_sources)]
12044    async fn test_only_v2_reward_tree_updates<D: TestableSqlDataSource>(
12045        _d: PhantomData<D>,
12046    ) -> anyhow::Result<()> {
12047        // Rewards accrue in the v2 reward Merkle tree, and the v1 tree stays empty.
12048        const EPOCH_HEIGHT: u64 = 10;
12049
12050        let network_config = TestConfigBuilder::default()
12051            .epoch_height(EPOCH_HEIGHT)
12052            .build();
12053
12054        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
12055
12056        tracing::info!("API PORT = {api_port}");
12057        const NUM_NODES: usize = 5;
12058
12059        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
12060
12061        let config = TestNetworkConfigBuilder::with_num_nodes()
12062            .api_config(D::options(
12063                &storage[0],
12064                Options::with_port(api_port).catchup(Default::default()),
12065            ))
12066            .network_config(network_config)
12067            .persistences(persistence.clone())
12068            .build();
12069        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
12070
12071        let mut events = network.peers[2].event_stream();
12072        // wait for 4 epochs
12073        wait_for_epochs(&mut events, EPOCH_HEIGHT, 4).await;
12074
12075        let validated_state = network.server.decided_state().await.unwrap();
12076        let v1_tree = &validated_state.reward_merkle_tree_v1;
12077        assert!(
12078            v1_tree.num_leaves() == 0,
12079            "v1 reward tree tree is not empty"
12080        );
12081        let v2_tree = &validated_state.reward_merkle_tree_v2;
12082        assert!(v2_tree.num_leaves() > 0, "v2 reward tree tree is empty");
12083
12084        network.stop_consensus().await;
12085        Ok(())
12086    }
12087
12088    #[apply(sql_data_sources)]
12089    pub(crate) async fn test_state_cert_query<D: TestableSqlDataSource>(_d: PhantomData<D>) {
12090        const TEST_EPOCH_HEIGHT: u64 = 10;
12091        const TEST_EPOCHS: u64 = 5;
12092
12093        let network_config = TestConfigBuilder::default()
12094            .epoch_height(TEST_EPOCH_HEIGHT)
12095            .build();
12096
12097        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
12098
12099        tracing::info!("API PORT = {api_port}");
12100        const NUM_NODES: usize = 2;
12101
12102        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
12103
12104        let config = TestNetworkConfigBuilder::with_num_nodes()
12105            .api_config(D::options(
12106                &storage[0],
12107                Options::with_port(api_port).catchup(Default::default()),
12108            ))
12109            .network_config(network_config)
12110            .persistences(persistence.clone())
12111            .build();
12112
12113        let network = TestNetwork::new(config, TEST_UPGRADE).await;
12114        let mut events = network.server.event_stream();
12115
12116        // Wait until 5 epochs have passed.
12117        loop {
12118            let event = events.next().await.unwrap();
12119            tracing::info!("Received event from handle: {event:?}");
12120
12121            if let CoordinatorEvent::NewDecide {
12122                leaf_infos: leaf_chain,
12123                ..
12124            } = event
12125            {
12126                println!(
12127                    "Decide event received: {:?}",
12128                    leaf_chain.first().unwrap().leaf.height()
12129                );
12130                if let Some(first_leaf) = leaf_chain.first() {
12131                    let height = first_leaf.leaf.height();
12132                    tracing::info!("Decide event received at height: {height}");
12133
12134                    if height >= TEST_EPOCHS * TEST_EPOCH_HEIGHT {
12135                        break;
12136                    }
12137                }
12138            }
12139        }
12140
12141        // Connect client.
12142        let client: Client<ClientErr, StaticVersion<0, 1>> =
12143            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
12144        client.connect(Some(Duration::from_secs(10))).await;
12145
12146        assert_state_certs(&client, 3..=TEST_EPOCHS, TEST_EPOCH_HEIGHT).await;
12147
12148        // The root gate runs only after a storage miss, so a stored cert is served whatever
12149        // this node has decided.
12150        const UNDECIDED_EPOCH: u64 = 1_000_000;
12151        let persistence = network.server.persistence();
12152        let cert = persistence
12153            .get_state_cert_by_epoch(TEST_EPOCHS)
12154            .await
12155            .unwrap()
12156            .expect("the query above stored this cert");
12157        persistence
12158            .insert_state_cert(UNDECIDED_EPOCH, cert.clone())
12159            .await
12160            .unwrap();
12161        let served = client
12162            .get::<StateCertQueryDataV2<SeqTypes>>(&format!(
12163                "availability/state-cert-v2/{UNDECIDED_EPOCH}"
12164            ))
12165            .send()
12166            .await
12167            .unwrap();
12168        assert_eq!(served.0, cert);
12169    }
12170
12171    /// Light client provers and catching-up nodes still fetch the state certificates of
12172    /// mainnet's epochs before 0.6, whose blocks only legacy consensus produced.
12173    #[rstest]
12174    #[case::v3("v3")]
12175    #[case::v4("v4")]
12176    #[test_log::test(tokio::test(flavor = "multi_thread"))]
12177    async fn test_legacy_state_certs<D: TestableSqlDataSource>(
12178        #[values(PhantomData::<Postgres>, PhantomData::<Sqlite>)] _d: PhantomData<D>,
12179        #[case] chain: &str,
12180    ) -> anyhow::Result<()> {
12181        let chain = LegacyChain::load(chain)?;
12182        let replay = chain.replay::<D>().await?;
12183        let (last_epoch, _) = chain.state_certs.last().unwrap();
12184        assert_state_certs(&replay.client, 3..=*last_epoch, chain.epoch_height()).await;
12185
12186        let peers = StatePeers::<SequencerApiVersion>::from_urls(
12187            vec![replay.url.clone()],
12188            Default::default(),
12189            Duration::from_secs(2),
12190            &NoMetrics,
12191        );
12192        for (epoch, cert) in &chain.state_certs {
12193            ensure!(
12194                peers.try_fetch_state_cert(0, *epoch).await? == *cert,
12195                "catchup of the epoch {epoch} state certificate"
12196            );
12197        }
12198        Ok(())
12199    }
12200
12201    /// Checks the v1 and v2 state certificates of `epochs` agree, and that each certifies its
12202    /// epoch root block.
12203    async fn assert_state_certs(
12204        client: &Client<ClientErr, StaticVersion<0, 1>>,
12205        epochs: std::ops::RangeInclusive<u64>,
12206        epoch_height: u64,
12207    ) {
12208        for i in epochs {
12209            let state_query_data_v2 = client
12210                .get::<StateCertQueryDataV2<SeqTypes>>(&format!("availability/state-cert-v2/{i}"))
12211                .send()
12212                .await
12213                .unwrap();
12214            let state_cert_v2 = state_query_data_v2.0.clone();
12215            tracing::info!("state_cert_v2: {state_cert_v2:?}");
12216            assert_eq!(state_cert_v2.epoch.u64(), i);
12217            assert_eq!(
12218                state_cert_v2.light_client_state.block_height,
12219                i * epoch_height - 5
12220            );
12221            let block_height = state_cert_v2.light_client_state.block_height;
12222
12223            let header: Header = client
12224                .get(&format!("availability/header/{block_height}"))
12225                .send()
12226                .await
12227                .unwrap();
12228
12229            // Headers commit to an auth root from 0.4 on.
12230            if header.version() >= DRB_AND_HEADER_UPGRADE_VERSION {
12231                let auth_root = state_cert_v2.auth_root;
12232                let header_auth_root = header.auth_root().unwrap();
12233                if auth_root.is_zero() || header_auth_root.is_zero() {
12234                    panic!("auth root shouldn't be zero");
12235                }
12236
12237                assert_eq!(auth_root, header_auth_root, "auth root mismatch");
12238            }
12239
12240            let state_query_data_v1 = client
12241                .get::<StateCertQueryDataV1<SeqTypes>>(&format!("availability/state-cert/{i}"))
12242                .send()
12243                .await
12244                .unwrap();
12245
12246            let state_cert_v1 = state_query_data_v1.0.clone();
12247            tracing::info!("state_cert_v1: {state_cert_v1:?}");
12248            assert_eq!(state_query_data_v1, state_query_data_v2.into());
12249        }
12250    }
12251
12252    /// Test state certificate catchup functionality by simulating a node that falls behind and needs
12253    /// to catch up. This test starts a 5-node network with epoch height 10, waits for 3 epochs to
12254    /// pass, then removes and restarts node 0 with a fresh storage. The
12255    /// restarted node catches up for the missing state certificates.
12256    #[apply(sql_data_sources)]
12257    pub(crate) async fn test_state_cert_catchup<D: TestableSqlDataSource>(_d: PhantomData<D>) {
12258        const EPOCH_HEIGHT: u64 = 10;
12259
12260        let network_config = TestConfigBuilder::default()
12261            .epoch_height(EPOCH_HEIGHT)
12262            .build();
12263
12264        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
12265
12266        tracing::info!("API PORT = {api_port}");
12267        const NUM_NODES: usize = 5;
12268
12269        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
12270
12271        let config = TestNetworkConfigBuilder::with_num_nodes()
12272            .api_config(D::options(&storage[0], Options::with_port(api_port)))
12273            .network_config(network_config)
12274            .persistences(persistence.clone())
12275            .build();
12276        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
12277        let state = network.genesis_state(0);
12278
12279        let mut events = network.peers[2].event_stream();
12280        // Wait until at least 5 epochs have passed
12281        wait_for_epochs(&mut events, EPOCH_HEIGHT, 3).await;
12282
12283        // Remove peer 0 and restart it with the query module enabled.
12284        // Adding an additional node to the test network is not straight forward,
12285        // as the keys have already been initialized in the config above.
12286        // So, we remove this node and re-add it using the same index.
12287        // Await the shutdown so the node's cliquenet listener releases its
12288        // port before the replacement node binds the same address.
12289        network.peers.remove(0).shut_down().await;
12290
12291        let new_storage = D::create_storage().await;
12292        let new_persistence = <D as TestableSequencerDataSource>::persistence_options(&new_storage);
12293
12294        let node_0_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
12295        tracing::info!("node_0_port {node_0_port}");
12296        let opt = Options::with_port(node_0_port).query_sql(
12297            Query {
12298                peers: vec![format!("http://localhost:{api_port}").parse().unwrap()],
12299                ..Query::test()
12300            },
12301            D::tmp_options(&new_storage),
12302        );
12303        let node_0 = opt
12304            .clone()
12305            .serve(|metrics, consumer, storage| {
12306                let cfg = network.cfg.clone();
12307                let new_persistence = new_persistence.clone();
12308                let state = state.clone();
12309                async move {
12310                    Ok(cfg
12311                        .init_node(
12312                            1,
12313                            state,
12314                            new_persistence.clone(),
12315                            Some(StatePeers::<StaticVersion<0, 1>>::from_urls(
12316                                vec![format!("http://localhost:{api_port}").parse().unwrap()],
12317                                Default::default(),
12318                                Duration::from_secs(2),
12319                                &NoMetrics,
12320                            )),
12321                            storage,
12322                            &*metrics,
12323                            test_helpers::STAKE_TABLE_CAPACITY_FOR_TEST,
12324                            consumer,
12325                            TEST_UPGRADE,
12326                            Default::default(),
12327                        )
12328                        .await)
12329                }
12330                .boxed()
12331            })
12332            .await
12333            .unwrap();
12334        node_0.start_consensus().await;
12335
12336        let mut events = node_0.event_stream();
12337        // Wait until at least 5 epochs have passed
12338        wait_for_epochs(&mut events, EPOCH_HEIGHT, 5).await;
12339
12340        let client: Client<ClientErr, StaticVersion<0, 1>> =
12341            Client::new(format!("http://localhost:{node_0_port}").parse().unwrap());
12342        client.connect(Some(Duration::from_secs(60))).await;
12343
12344        for epoch in 3..=5 {
12345            let state_cert = client
12346                .get::<StateCertQueryDataV2<SeqTypes>>(&format!(
12347                    "availability/state-cert-v2/{epoch}"
12348                ))
12349                .send()
12350                .await
12351                .unwrap();
12352            assert_eq!(state_cert.0.epoch.u64(), epoch);
12353        }
12354    }
12355
12356    #[apply(sql_data_sources)]
12357    async fn test_integration_commission_updates<D: TestableSqlDataSource>(
12358        _d: PhantomData<D>,
12359    ) -> anyhow::Result<()> {
12360        const NUM_NODES: usize = 3;
12361        const EPOCH_HEIGHT: u64 = 10;
12362
12363        // Use version that supports epochs (V3 or V4)
12364
12365        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
12366
12367        // Initialize storage for nodes
12368        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
12369
12370        // Configure test network with epochs
12371        let network_config = TestConfigBuilder::default()
12372            .epoch_height(EPOCH_HEIGHT)
12373            .build();
12374
12375        // At 0.6 only validators with a registered x25519 key and p2p address are eligible, and
12376        // only the V3 contract registers them.
12377        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
12378            .api_config(D::options(&storage[0], Options::with_port(api_port)))
12379            .network_config(network_config.clone())
12380            .persistences(persistence.clone())
12381            .pos_hook(
12382                DelegationConfig::NoSelfDelegation,
12383                StakeTableContractVersion::V3,
12384                TEST_UPGRADE,
12385            )
12386            .await
12387            .unwrap()
12388            .build();
12389
12390        let network = TestNetwork::new(config, TEST_UPGRADE).await;
12391        let contracts = network.contracts.unwrap();
12392        let st_addr = contracts.address(Contract::StakeTableProxy).unwrap();
12393
12394        // Update only once epoch 3 has started: by then the stake tables of epochs 3 and 4 are
12395        // fixed, so both carry the old commissions.
12396        let mut events = network.peers[0].event_stream();
12397        wait_for_epochs(&mut events, EPOCH_HEIGHT, 2).await;
12398
12399        let mut commissions = vec![];
12400        for (i, (validator, provider)) in
12401            network_config.validator_providers().into_iter().enumerate()
12402        {
12403            let commission = fetch_commission(provider.clone(), st_addr, validator).await?;
12404            let new_commission = match i {
12405                0 => 0u16,
12406                1 => commission.to_evm() + 500u16,
12407                2 => commission.to_evm() - 100u16,
12408                _ => unreachable!(),
12409            }
12410            .try_into()?;
12411            commissions.push((validator, commission, new_commission));
12412            tracing::info!(%validator, %commission, %new_commission, "Update commission");
12413            update_commission(provider, st_addr, new_commission)
12414                .await?
12415                .get_receipt()
12416                .await?;
12417        }
12418
12419        let client: Client<ClientErr, SequencerApiVersion> =
12420            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
12421        let commissions_at = async |epoch: u64| {
12422            let validators = client
12423                .get::<AuthenticatedValidatorMap>(&format!("node/validators/{epoch}"))
12424                .send()
12425                .await
12426                .expect("validators");
12427            commissions
12428                .iter()
12429                .map(|(val, ..)| validators.get(val).unwrap().commission)
12430                .collect::<Vec<_>>()
12431        };
12432        let old = commissions
12433            .iter()
12434            .map(|(_, old_comm, _)| old_comm.to_evm())
12435            .collect::<Vec<_>>();
12436        let new = commissions
12437            .iter()
12438            .map(|(.., new_comm)| new_comm.to_evm())
12439            .collect::<Vec<_>>();
12440        assert_eq!(commissions_at(4).await, old);
12441
12442        // Which epoch first has all the new commissions depends on when each update finalizes on
12443        // L1 relative to the epoch roots, so search for it. Epochs in between can mix old and new.
12444        let mut first_new_epoch = 5;
12445        loop {
12446            wait_for_epochs(&mut events, EPOCH_HEIGHT, first_new_epoch - 1).await;
12447            if commissions_at(first_new_epoch).await == new {
12448                break;
12449            }
12450            first_new_epoch += 1;
12451            assert!(
12452                first_new_epoch <= 10,
12453                "the new commissions never took effect"
12454            );
12455        }
12456
12457        // An epoch's rewards are applied at the last block of the next epoch, so the rewards
12458        // applied at the end of `first_new_epoch + 1` are the first under the new commissions.
12459        wait_for_epochs(&mut events, EPOCH_HEIGHT, first_new_epoch + 1).await;
12460        let last_block_with_old_commissions = EPOCH_HEIGHT * first_new_epoch;
12461        let block_with_new_commissions = EPOCH_HEIGHT * (first_new_epoch + 1);
12462        wait_until_block_height(
12463            &client,
12464            "reward-state-v2/block-height",
12465            block_with_new_commissions + 1,
12466        )
12467        .await;
12468        let mut new_amounts = vec![];
12469        for (val, ..) in commissions {
12470            let before = client
12471                .get::<Option<RewardAmount>>(&format!(
12472                    "reward-state-v2/reward-balance/{last_block_with_old_commissions}/{val}"
12473                ))
12474                .send()
12475                .await?
12476                .unwrap();
12477            let after = client
12478                .get::<Option<RewardAmount>>(&format!(
12479                    "reward-state-v2/reward-balance/{block_with_new_commissions}/{val}"
12480                ))
12481                .send()
12482                .await?
12483                .unwrap();
12484            new_amounts.push(after - before);
12485        }
12486
12487        let tolerance = U256::from(10 * EPOCH_HEIGHT).into();
12488        // validator zero got new new rewards except remainders
12489        assert!(new_amounts[0] < tolerance);
12490
12491        // other validators are still receiving rewards
12492        assert!(new_amounts[1] + new_amounts[2] > tolerance);
12493
12494        Ok(())
12495    }
12496
12497    /// Call `updateNetworkConfig` on one validator and verify the indexer surfaces the new x25519
12498    /// key and p2p address in the validator map.
12499    #[apply(sql_data_sources)]
12500    async fn test_integration_update_fast_finality_network_config<D: TestableSqlDataSource>(
12501        _d: PhantomData<D>,
12502    ) -> anyhow::Result<()> {
12503        // Peers switch to a validator's new key and address as soon as the update lands, and none
12504        // can reach the updated one at its made-up address, so the others must keep a quorum.
12505        const NUM_NODES: usize = 4;
12506        const EPOCH_HEIGHT: u64 = 10;
12507
12508        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
12509
12510        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
12511
12512        let network_config = TestConfigBuilder::default()
12513            .epoch_height(EPOCH_HEIGHT)
12514            .build();
12515
12516        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
12517            .api_config(D::options(&storage[0], Options::with_port(api_port)))
12518            .network_config(network_config.clone())
12519            .persistences(persistence.clone())
12520            .pos_hook(
12521                DelegationConfig::EqualAmounts,
12522                StakeTableContractVersion::V3,
12523                TEST_UPGRADE,
12524            )
12525            .await
12526            .unwrap()
12527            .build();
12528
12529        let network = TestNetwork::new(config, TEST_UPGRADE).await;
12530        let contracts = network.contracts.unwrap();
12531        let st_addr = contracts.address(Contract::StakeTableProxy).unwrap();
12532
12533        let (validator, validator_provider) = network_config
12534            .validator_providers()
12535            .into_iter()
12536            .next()
12537            .unwrap();
12538        let x25519_key = x25519::Keypair::generate().unwrap().public_key();
12539        let p2p_addr: NetAddr = "127.0.0.1:9000".parse().unwrap();
12540        update_network_config(validator_provider, st_addr, x25519_key, p2p_addr.clone())
12541            .await?
12542            .get_receipt()
12543            .await?;
12544
12545        let current_epoch = network.peers[0]
12546            .decided_leaf()
12547            .await
12548            .epoch(EPOCH_HEIGHT)
12549            .unwrap();
12550        let target_epoch = current_epoch.u64() + 3;
12551        let mut events = network.peers[0].event_stream();
12552        wait_for_epochs(&mut events, EPOCH_HEIGHT, target_epoch).await;
12553
12554        let client: Client<ClientErr, SequencerApiVersion> =
12555            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
12556        let validators = client
12557            .get::<AuthenticatedValidatorMap>(&format!("node/validators/{target_epoch}"))
12558            .send()
12559            .await
12560            .expect("validators");
12561        let v = validators.get(&validator).expect("validator present");
12562        assert_eq!(v.x25519_key, Some(x25519_key));
12563        assert_eq!(v.p2p_addr, Some(p2p_addr));
12564
12565        Ok(())
12566    }
12567
12568    /// Mainnet's validators set their x25519 keys and p2p addresses with the V3 stake table
12569    /// contract, after epochs run on V1 and V2.
12570    #[apply(sql_data_sources)]
12571    async fn test_legacy_network_config_update<D: TestableSqlDataSource>(
12572        _d: PhantomData<D>,
12573    ) -> anyhow::Result<()> {
12574        let chain = LegacyChain::load("v4")?;
12575        let update = chain
12576            .network_config_update
12577            .clone()
12578            .context("the v4 recording registered a network config")?;
12579        let replay = chain.replay::<D>().await?;
12580        let last_epoch = chain.last_epoch();
12581        let validators = |epoch: u64| {
12582            let client = replay.client.clone();
12583            async move {
12584                client
12585                    .get::<AuthenticatedValidatorMap>(&format!("node/validators/{epoch}"))
12586                    .send()
12587                    .await
12588            }
12589        };
12590
12591        // The update reaches the stake table a few epochs after the V3 upgrade in epoch 4.
12592        let mut first_seen = None;
12593        // The stake table takes effect in epoch 3.
12594        for epoch in 3..=last_epoch {
12595            let validators = validators(epoch).await?;
12596            ensure!(!validators.is_empty(), "epoch {epoch} has validators");
12597            let configured = validators
12598                .values()
12599                .filter(|v| v.x25519_key.is_some() || v.p2p_addr.is_some())
12600                .collect::<Vec<_>>();
12601            if first_seen.is_none() {
12602                if configured.is_empty() {
12603                    continue;
12604                }
12605                first_seen = Some(epoch);
12606            }
12607            let [v] = configured.as_slice() else {
12608                bail!(
12609                    "epoch {epoch}: {} validators have a network config",
12610                    configured.len()
12611                );
12612            };
12613            ensure!(
12614                v.account == update.validator
12615                    && v.x25519_key.as_ref() == Some(&update.x25519_key)
12616                    && v.p2p_addr.as_ref() == Some(&update.p2p_addr),
12617                "epoch {epoch} shows the recorded network config update, not {v:?}"
12618            );
12619        }
12620        let first_seen = first_seen.context("no epoch shows the network config update")?;
12621        ensure!(
12622            first_seen > 4,
12623            "a V1 or V2 contract registers no network config, but epoch {first_seen} has one"
12624        );
12625        Ok(())
12626    }
12627
12628    /// Assert the endpoint returns a 2xx status and a valid JSON body.
12629    async fn assert_json_endpoint(
12630        http: &reqwest::Client,
12631        api_port: u16,
12632        path: &str,
12633    ) -> anyhow::Result<()> {
12634        let resp = http
12635            .get(format!("http://localhost:{api_port}/v1/{path}"))
12636            .send()
12637            .await?;
12638        let status = resp.status();
12639        assert!(
12640            status.is_success(),
12641            "v1/{path}: returned {status}, expected 2xx"
12642        );
12643        resp.json::<serde_json::Value>().await?;
12644        Ok(())
12645    }
12646
12647    /// Assert the endpoint returns a well-formed JSON body, without constraining the status.
12648    /// For routes where an error response is the expected outcome but its exact status is not
12649    /// part of the contract.
12650    async fn assert_json_body(
12651        http: &reqwest::Client,
12652        api_port: u16,
12653        path: &str,
12654    ) -> anyhow::Result<()> {
12655        http.get(format!("http://localhost:{api_port}/v1/{path}"))
12656            .send()
12657            .await?
12658            .json::<serde_json::Value>()
12659            .await?;
12660        Ok(())
12661    }
12662
12663    /// Assert the endpoint returns the expected HTTP status code.
12664    async fn assert_endpoint_status(
12665        http: &reqwest::Client,
12666        api_port: u16,
12667        path: &str,
12668        expected_status: u16,
12669    ) -> anyhow::Result<()> {
12670        let status = http
12671            .get(format!("http://localhost:{api_port}/v1/{path}"))
12672            .send()
12673            .await?
12674            .status()
12675            .as_u16();
12676        assert_eq!(
12677            status, expected_status,
12678            "v1/{path}: should return {expected_status}, got {status}"
12679        );
12680        Ok(())
12681    }
12682
12683    /// Assert the endpoint returns a 2xx status, without requiring a JSON body. Used for
12684    /// endpoints whose content is not JSON or varies between calls (e.g. live metrics).
12685    async fn assert_endpoint_ok(
12686        http: &reqwest::Client,
12687        api_port: u16,
12688        path: &str,
12689    ) -> anyhow::Result<()> {
12690        let status = http
12691            .get(format!("http://localhost:{api_port}/v1/{path}"))
12692            .send()
12693            .await?
12694            .status();
12695        assert!(
12696            status.is_success(),
12697            "v1/{path}: returned {status}, expected 2xx"
12698        );
12699        Ok(())
12700    }
12701
12702    /// Assert an endpoint that fails via `ApiError` returns the expected status and the
12703    /// `{"Custom":{"message","status"}}` error envelope that existing clients parse.
12704    async fn assert_error_body(
12705        http: &reqwest::Client,
12706        api_port: u16,
12707        path: &str,
12708        expected_status: u16,
12709    ) -> anyhow::Result<()> {
12710        let resp = http
12711            .get(format!("http://localhost:{api_port}/v1/{path}"))
12712            .send()
12713            .await?;
12714        let status = resp.status().as_u16();
12715        let body: serde_json::Value = resp.json().await?;
12716        assert_eq!(status, expected_status, "v1/{path}: status");
12717        let custom = body
12718            .get("Custom")
12719            .unwrap_or_else(|| panic!("v1/{path}: error body missing Custom envelope: {body}"));
12720        assert_eq!(
12721            custom.get("status").and_then(|s| s.as_u64()),
12722            Some(u64::from(expected_status)),
12723            "v1/{path}: envelope status: {body}"
12724        );
12725        assert!(
12726            custom.get("message").is_some_and(|m| m.is_string()),
12727            "v1/{path}: envelope message missing: {body}"
12728        );
12729        Ok(())
12730    }
12731
12732    /// POST a VBS-binary body and assert the server accepts it.
12733    ///
12734    /// VBS (Versioned Binary Serialization) is what production peer-catchup and
12735    /// `submit-transactions` clients use via `http_client::Request::body_binary`. This helper
12736    /// catches regressions where the handler accepts only JSON.
12737    async fn assert_post_binary<B: serde::Serialize>(
12738        http: &reqwest::Client,
12739        api_port: u16,
12740        path: &str,
12741        body: &B,
12742    ) -> anyhow::Result<()> {
12743        use vbs::{BinarySerializer, Serializer, version::StaticVersion};
12744        let payload = Serializer::<StaticVersion<0, 1>>::serialize(body)?;
12745        let resp = http
12746            .post(format!("http://localhost:{api_port}/v1/{path}"))
12747            .header("Content-Type", "application/octet-stream")
12748            .header("Accept", "application/octet-stream")
12749            .body(payload)
12750            .send()
12751            .await?;
12752        let status = resp.status();
12753        assert!(
12754            status.is_success(),
12755            "v1/{path}: binary POST returned {status}, expected 2xx"
12756        );
12757        Ok(())
12758    }
12759
12760    /// Connect to the WebSocket endpoint, collect up to 10 messages, and assert that at least 2
12761    /// of them are valid JSON.
12762    async fn assert_ws_endpoint(api_port: u16, path: &str) -> anyhow::Result<()> {
12763        use std::time::Duration;
12764
12765        use futures::StreamExt as _;
12766        use tokio::time::timeout;
12767        use tokio_tungstenite::{connect_async, tungstenite::Message};
12768
12769        let url = format!("ws://localhost:{api_port}/v1/{path}");
12770        let (mut ws, _) = connect_async(&url).await?;
12771        let mut messages = Vec::new();
12772        while messages.len() < 10 {
12773            match timeout(Duration::from_millis(500), ws.next()).await {
12774                Ok(Some(Ok(Message::Text(text)))) => {
12775                    if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
12776                        messages.push(v);
12777                    }
12778                },
12779                _ => break,
12780            }
12781        }
12782
12783        assert!(
12784            messages.len() >= 2,
12785            "v1/{path}: expected >=2 JSON messages from the stream, got {}",
12786            messages.len(),
12787        );
12788        Ok(())
12789    }
12790
12791    /// Same as `assert_ws_endpoint` but exercises the binary (`Accept: application/octet-stream`)
12792    /// path that our clients use by default. Asserts the server sends `Message::Binary` frames
12793    /// carrying VBS-encoded payloads.
12794    async fn assert_ws_endpoint_binary(api_port: u16, path: &str) -> anyhow::Result<()> {
12795        use std::time::Duration;
12796
12797        use futures::StreamExt as _;
12798        use tokio::time::timeout;
12799        use tokio_tungstenite::{
12800            connect_async,
12801            tungstenite::{client::IntoClientRequest, http::HeaderValue, protocol::Message},
12802        };
12803
12804        let url = format!("ws://localhost:{api_port}/v1/{path}");
12805        let mut req = url.as_str().into_client_request()?;
12806        req.headers_mut().insert(
12807            "Accept",
12808            HeaderValue::from_static("application/octet-stream"),
12809        );
12810        let (mut ws, _) = connect_async(req).await?;
12811        let mut frames = Vec::new();
12812        while frames.len() < 3 {
12813            match timeout(Duration::from_millis(500), ws.next()).await {
12814                Ok(Some(Ok(Message::Binary(bytes)))) => frames.push(bytes.to_vec()),
12815                _ => break,
12816            }
12817        }
12818
12819        assert!(
12820            !frames.is_empty(),
12821            "v1/{path}: no binary frames (Accept: application/octet-stream); handler likely \
12822             always sends text",
12823        );
12824        Ok(())
12825    }
12826
12827    #[rstest]
12828    #[case::postgres(PhantomData::<Postgres>)]
12829    #[case::sqlite(PhantomData::<Sqlite>)]
12830    #[test_log::test]
12831    fn test_reward_proof_endpoint<D: TestableSqlDataSource>(#[case] _d: PhantomData<D>) {
12832        let test = async move {
12833            const EPOCH_HEIGHT: u64 = 10;
12834            const NUM_NODES: usize = 5;
12835
12836            let network_config = TestConfigBuilder::default()
12837                .epoch_height(EPOCH_HEIGHT)
12838                .build();
12839
12840            let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
12841            println!("API PORT = {api_port}");
12842
12843            let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
12844
12845            let api_opts = Options::with_port(api_port)
12846                .catchup(Default::default())
12847                .config(Default::default())
12848                .explorer(Default::default());
12849
12850            let config = TestNetworkConfigBuilder::with_num_nodes()
12851                .api_config(D::options(&storage[0], api_opts))
12852                .network_config(network_config.clone())
12853                .persistences(persistence.clone())
12854                .build();
12855
12856            let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
12857
12858            // wait for 4 epochs
12859            let mut events = network.server.event_stream();
12860            wait_for_epochs(&mut events, EPOCH_HEIGHT, 4).await;
12861
12862            let url = format!("http://localhost:{api_port}").parse().unwrap();
12863            let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
12864
12865            let validated_state = network.server.decided_state().await.unwrap();
12866            let decided_leaf = network.server.decided_leaf().await;
12867            // Reward trees are persisted only at epoch boundaries, and rewards are distributed only
12868            // there, so the last boundary holds the same tree as the decided state.
12869            let height = decided_leaf.height() / EPOCH_HEIGHT * EPOCH_HEIGHT;
12870
12871            // Submit two transactions to the same namespace in separate blocks
12872            // so the namespace-filtered WS stream produces ≥2 messages.
12873            // Submitting both at once risks the builder batching them into a
12874            // single block; submitting sequentially (wait between) guarantees
12875            // different blocks so the second wait_for_decide_on_handle doesn't
12876            // hang looking for an event that was already consumed by the first.
12877            let avail_ns = NamespaceId::from(42_u32);
12878            let avail_tx = Transaction::new(avail_ns, vec![1, 2, 3]);
12879            network
12880                .server
12881                .submit_transaction(avail_tx.clone())
12882                .await
12883                .unwrap();
12884            let (avail_block, _) = wait_for_decide_on_handle(&mut events, &avail_tx).await;
12885
12886            // Submit the second transaction only after the first is decided,
12887            // ensuring it lands in a strictly later block.
12888            let avail_tx2 = Transaction::new(avail_ns, vec![4, 5, 6]);
12889            network
12890                .server
12891                .submit_transaction(avail_tx2.clone())
12892                .await
12893                .unwrap();
12894            wait_for_decide_on_handle(&mut events, &avail_tx2).await;
12895
12896            wait_until_block_height(&client, "reward-state-v2/block-height", height).await;
12897            // Wait for the availability query service to index avail_block.
12898            wait_until_block_height(&client, "node/block-height", avail_block).await;
12899
12900            // Sample a fee account for the fee-state comparisons below.
12901            // `validated_state` was captured before the fee-paying blocks above were
12902            // decided, so its fee tree can still be empty; poll the decided state while
12903            // consensus is still running (it is frozen after stop_consensus). The
12904            // decided state can also contain accounts added after `avail_block`, so
12905            // only accept an account provable at the `avail_block` snapshot queried
12906            // in the comparisons.
12907            let sample_start = Instant::now();
12908            let fee_account = 'fee_account: loop {
12909                // A node can decide a leaf before it has validated that leaf's state itself.
12910                let state = network.server.decided_state().await;
12911                for (addr, _) in state.iter().flat_map(|state| state.fee_merkle_tree.iter()) {
12912                    if client
12913                        .get::<MerkleProof<FeeAmount, FeeAccount, Sha3Node, 256>>(&format!(
12914                            "fee-state/{avail_block}/{addr}"
12915                        ))
12916                        .send()
12917                        .await
12918                        .is_ok()
12919                    {
12920                        break 'fee_account *addr;
12921                    }
12922                }
12923                assert!(
12924                    sample_start.elapsed() < Duration::from_secs(30),
12925                    "no fee account provable at avail_block {avail_block} after 30s"
12926                );
12927                sleep(Duration::from_millis(500)).await;
12928            };
12929
12930            network.stop_consensus().await;
12931
12932            let http = reqwest::Client::new();
12933
12934            for (address, _) in validated_state.reward_merkle_tree_v2.iter() {
12935                let (_, expected_proof) = validated_state
12936                    .reward_merkle_tree_v2
12937                    .lookup(*address)
12938                    .expect_ok()
12939                    .unwrap();
12940
12941                let res = client
12942                    .get::<RewardAccountQueryDataV2>(&format!(
12943                        "reward-state-v2/proof/{height}/{address}"
12944                    ))
12945                    .send()
12946                    .await
12947                    .unwrap();
12948
12949                match res.proof.proof.clone() {
12950                    RewardMerkleProofV2::Presence(p) => {
12951                        assert_eq!(
12952                            p, expected_proof,
12953                            "Proof mismatch for V2 at {height}, addr={address}"
12954                        );
12955                    },
12956                    other => panic!(
12957                        "Expected Present proof for V2 at {height}, addr={address}, got {other:?}"
12958                    ),
12959                }
12960
12961                let reward_claim_input = client
12962                    .get::<RewardClaimInput>(&format!(
12963                        "reward-state-v2/reward-claim-input/{height}/{address}"
12964                    ))
12965                    .send()
12966                    .await
12967                    .unwrap();
12968
12969                assert_eq!(reward_claim_input, res.to_reward_claim_input()?);
12970
12971                // Behavior relied on by scripts/claim-rewards-loop: an account with no
12972                // rewards yields 404; any other error status makes the claim loop exit and
12973                // process-compose tear down the whole demo.
12974                let absent = alloy::primitives::Address::with_last_byte(0xaa);
12975                assert!(
12976                    validated_state
12977                        .reward_merkle_tree_v2
12978                        .iter()
12979                        .all(|(addr, _)| addr.0 != absent),
12980                    "sentinel address unexpectedly present in reward tree"
12981                );
12982                let err = client
12983                    .get::<RewardClaimInput>(&format!(
12984                        "reward-state-v2/reward-claim-input/{height}/{absent}"
12985                    ))
12986                    .send()
12987                    .await
12988                    .unwrap_err();
12989                assert_matches!(err, ClientErr { status, .. } if status == StatusCode::NOT_FOUND);
12990
12991                // Smoke-check each per-address endpoint under reward-state-v2.
12992                assert_json_endpoint(
12993                    &http,
12994                    api_port,
12995                    &format!("reward-state-v2/proof/{height}/{address}"),
12996                )
12997                .await?;
12998                assert_json_endpoint(
12999                    &http,
13000                    api_port,
13001                    &format!("reward-state-v2/reward-claim-input/{height}/{address}"),
13002                )
13003                .await?;
13004                assert_json_endpoint(
13005                    &http,
13006                    api_port,
13007                    &format!("reward-state-v2/reward-balance/{height}/{address}"),
13008                )
13009                .await?;
13010                assert_json_endpoint(
13011                    &http,
13012                    api_port,
13013                    &format!("reward-state-v2/proof/latest/{address}"),
13014                )
13015                .await?;
13016                assert_json_endpoint(
13017                    &http,
13018                    api_port,
13019                    &format!("reward-state-v2/reward-balance/latest/{address}"),
13020                )
13021                .await?;
13022
13023                // The reward-state mount shares its handlers with reward-state-v2 for
13024                // backwards compatibility, so these two routes hit the same v2-tree-backed
13025                // handlers as the pair above, just under reward-state.
13026                assert_json_endpoint(
13027                    &http,
13028                    api_port,
13029                    &format!("reward-state/proof/latest/{address}"),
13030                )
13031                .await?;
13032                assert_json_endpoint(
13033                    &http,
13034                    api_port,
13035                    &format!("reward-state/reward-balance/latest/{address}"),
13036                )
13037                .await?;
13038            }
13039
13040            let (address, _) = validated_state
13041                .reward_merkle_tree_v2
13042                .iter()
13043                .next()
13044                .expect("a proof-of-stake network has reward accounts");
13045            check_reward_state_v2_parity(&client, height, address.0).await;
13046
13047            assert_json_endpoint(
13048                &http,
13049                api_port,
13050                &format!("reward-state-v2/reward-amounts/{height}/0/1000"),
13051            )
13052            .await?;
13053            assert_json_endpoint(
13054                &http,
13055                api_port,
13056                &format!("reward-state-v2/reward-merkle-tree-v2/{height}"),
13057            )
13058            .await?;
13059            assert_json_endpoint(
13060                &http,
13061                api_port,
13062                &format!("reward-state/reward-amounts/{height}/0/1000"),
13063            )
13064            .await?;
13065            assert_json_endpoint(
13066                &http,
13067                api_port,
13068                &format!("reward-state/reward-merkle-tree-v2/{height}"),
13069            )
13070            .await?;
13071
13072            // Merklized-state `get_path` routes, inherited by both reward mounts from
13073            // the legacy `hotshot-query-service` merklized-state base routes (mirrors the block-state /
13074            // fee-state checks below). Nothing in this codebase populates the generic
13075            // merklized-state tables for the reward trees today; the reward-state modules
13076            // persist snapshots via the separate `persist_tree`/`load_tree` bincode-blob
13077            // mechanism instead, so these routes fail in practice. We only assert that both
13078            // mounts, in both height and commit form, return well-formed JSON.
13079            let reward_address = validated_state
13080                .reward_merkle_tree_v2
13081                .iter()
13082                .next()
13083                .map(|(addr, _)| *addr)
13084                .expect("reward tree should have at least one account");
13085            let reward_header: Header = client
13086                .get(&format!("availability/header/{height}"))
13087                .send()
13088                .await
13089                .unwrap();
13090            let reward_mt_commit = match reward_header.reward_merkle_tree_root() {
13091                either::Either::Left(commit) => commit.to_string(),
13092                either::Either::Right(commit) => commit.to_string(),
13093            };
13094            for mount in ["reward-state", "reward-state-v2"] {
13095                assert_json_body(
13096                    &http,
13097                    api_port,
13098                    &format!("{mount}/{height}/{reward_address}"),
13099                )
13100                .await?;
13101                assert_json_body(
13102                    &http,
13103                    api_port,
13104                    &format!("{mount}/commit/{reward_mt_commit}/{reward_address}"),
13105                )
13106                .await?;
13107            }
13108
13109            // Availability v1 routes.
13110
13111            // Namespace proof by height
13112            assert_json_endpoint(
13113                &http,
13114                api_port,
13115                &format!("availability/block/{avail_block}/namespace/{avail_ns}"),
13116            )
13117            .await?;
13118
13119            // Namespace proof by block hash and payload hash
13120            let avail_header: Header = client
13121                .get(&format!("availability/header/{avail_block}"))
13122                .send()
13123                .await
13124                .unwrap();
13125            assert_json_endpoint(
13126                &http,
13127                api_port,
13128                &format!(
13129                    "availability/block/hash/{}/namespace/{avail_ns}",
13130                    avail_header.commit()
13131                ),
13132            )
13133            .await?;
13134            assert_json_endpoint(
13135                &http,
13136                api_port,
13137                &format!(
13138                    "availability/block/payload-hash/{}/namespace/{avail_ns}",
13139                    avail_header.payload_commitment()
13140                ),
13141            )
13142            .await?;
13143
13144            // Namespace proof range
13145            assert_json_endpoint(
13146                &http,
13147                api_port,
13148                &format!(
13149                    "availability/block/{avail_block}/{}/namespace/{avail_ns}",
13150                    avail_block + 1
13151                ),
13152            )
13153            .await?;
13154
13155            // State certificate endpoints (epoch 1 is complete after 4 epochs)
13156            assert_json_endpoint(&http, api_port, "availability/state-cert/1").await?;
13157            assert_json_endpoint(&http, api_port, "availability/state-cert-v2/1").await?;
13158
13159            // HotShot availability endpoints: leaf, header, block, payload, vid/common, etc.
13160            let avail_leaf: LeafQueryData<SeqTypes> = client
13161                .get(&format!("availability/leaf/{avail_block}"))
13162                .send()
13163                .await
13164                .unwrap();
13165            let leaf_hash = avail_leaf.hash();
13166            let block_hash = avail_header.commit();
13167            let payload_hash = avail_header.payload_commitment();
13168
13169            // Leaf endpoints
13170            assert_json_endpoint(&http, api_port, &format!("availability/leaf/{avail_block}"))
13171                .await?;
13172            assert_json_endpoint(
13173                &http,
13174                api_port,
13175                &format!("availability/leaf/hash/{leaf_hash}"),
13176            )
13177            .await?;
13178            assert_json_endpoint(
13179                &http,
13180                api_port,
13181                &format!("availability/leaf/{avail_block}/{}", avail_block + 1),
13182            )
13183            .await?;
13184
13185            // Header endpoints
13186            assert_json_endpoint(
13187                &http,
13188                api_port,
13189                &format!("availability/header/{avail_block}"),
13190            )
13191            .await?;
13192            assert_json_endpoint(
13193                &http,
13194                api_port,
13195                &format!("availability/header/hash/{block_hash}"),
13196            )
13197            .await?;
13198            assert_json_endpoint(
13199                &http,
13200                api_port,
13201                &format!("availability/header/payload-hash/{payload_hash}"),
13202            )
13203            .await?;
13204            assert_json_endpoint(
13205                &http,
13206                api_port,
13207                &format!("availability/header/{avail_block}/{}", avail_block + 1),
13208            )
13209            .await?;
13210
13211            // Block endpoints
13212            assert_json_endpoint(
13213                &http,
13214                api_port,
13215                &format!("availability/block/{avail_block}"),
13216            )
13217            .await?;
13218            assert_json_endpoint(
13219                &http,
13220                api_port,
13221                &format!("availability/block/hash/{block_hash}"),
13222            )
13223            .await?;
13224            assert_json_endpoint(
13225                &http,
13226                api_port,
13227                &format!("availability/block/payload-hash/{payload_hash}"),
13228            )
13229            .await?;
13230            assert_json_endpoint(
13231                &http,
13232                api_port,
13233                &format!("availability/block/{avail_block}/{}", avail_block + 1),
13234            )
13235            .await?;
13236
13237            // Payload endpoints
13238            assert_json_endpoint(
13239                &http,
13240                api_port,
13241                &format!("availability/payload/{avail_block}"),
13242            )
13243            .await?;
13244            assert_json_endpoint(
13245                &http,
13246                api_port,
13247                &format!("availability/payload/hash/{payload_hash}"),
13248            )
13249            .await?;
13250            assert_json_endpoint(
13251                &http,
13252                api_port,
13253                &format!("availability/payload/block-hash/{block_hash}"),
13254            )
13255            .await?;
13256            assert_json_endpoint(
13257                &http,
13258                api_port,
13259                &format!("availability/payload/{avail_block}/{}", avail_block + 1),
13260            )
13261            .await?;
13262
13263            // VID common endpoints
13264            assert_json_endpoint(
13265                &http,
13266                api_port,
13267                &format!("availability/vid/common/{avail_block}"),
13268            )
13269            .await?;
13270            assert_json_endpoint(
13271                &http,
13272                api_port,
13273                &format!("availability/vid/common/hash/{block_hash}"),
13274            )
13275            .await?;
13276            assert_json_endpoint(
13277                &http,
13278                api_port,
13279                &format!("availability/vid/common/payload-hash/{payload_hash}"),
13280            )
13281            .await?;
13282            assert_json_endpoint(
13283                &http,
13284                api_port,
13285                &format!("availability/vid/common/{avail_block}/{}", avail_block + 1),
13286            )
13287            .await?;
13288
13289            // Transaction endpoints
13290            let tx_hash = avail_tx.commit();
13291            assert_json_endpoint(
13292                &http,
13293                api_port,
13294                &format!("availability/transaction/{avail_block}/0/noproof"),
13295            )
13296            .await?;
13297            assert_json_endpoint(
13298                &http,
13299                api_port,
13300                &format!("availability/transaction/hash/{tx_hash}/noproof"),
13301            )
13302            .await?;
13303            assert_json_endpoint(
13304                &http,
13305                api_port,
13306                &format!("availability/transaction/{avail_block}/0/proof"),
13307            )
13308            .await?;
13309            assert_json_endpoint(
13310                &http,
13311                api_port,
13312                &format!("availability/transaction/hash/{tx_hash}/proof"),
13313            )
13314            .await?;
13315            assert_json_endpoint(
13316                &http,
13317                api_port,
13318                &format!("availability/transaction/{avail_block}/0"),
13319            )
13320            .await?;
13321            assert_json_endpoint(
13322                &http,
13323                api_port,
13324                &format!("availability/transaction/hash/{tx_hash}"),
13325            )
13326            .await?;
13327
13328            // Block summary endpoints
13329            assert_json_endpoint(
13330                &http,
13331                api_port,
13332                &format!("availability/block/summary/{avail_block}"),
13333            )
13334            .await?;
13335            assert_json_endpoint(
13336                &http,
13337                api_port,
13338                &format!(
13339                    "availability/block/summaries/{avail_block}/{}",
13340                    avail_block + 1
13341                ),
13342            )
13343            .await?;
13344
13345            // Limits endpoint (static response)
13346            assert_json_endpoint(&http, api_port, "availability/limits").await?;
13347
13348            // Cert2 endpoint: a cert2 is stored only at the newest leaf of each decide, so
13349            // scan from `avail_block` for one. Every other height has none.
13350            let tip = client.get::<u64>("node/block-height").send().await?;
13351            let mut cert2_height = None;
13352            for height in avail_block..tip {
13353                let status = http
13354                    .get(format!(
13355                        "http://localhost:{api_port}/v1/availability/cert2/{height}"
13356                    ))
13357                    .send()
13358                    .await?
13359                    .status()
13360                    .as_u16();
13361                match status {
13362                    200 => {
13363                        cert2_height = Some(height);
13364                        break;
13365                    },
13366                    404 => {},
13367                    _ => panic!("v1/availability/cert2/{height}: returned {status}"),
13368                }
13369            }
13370            let cert2_height =
13371                cert2_height.with_context(|| format!("no cert2 stored in {avail_block}..{tip}"))?;
13372            assert_json_endpoint(
13373                &http,
13374                api_port,
13375                &format!("availability/cert2/{cert2_height}"),
13376            )
13377            .await?;
13378
13379            // WebSocket streaming endpoints.
13380            //
13381            // For unfiltered streams, start 10 blocks before avail_block so there are at
13382            // least 10 committed blocks ready to stream (consensus has already stopped).
13383            // For namespace-filtered streams, start at avail_block where the two submitted
13384            // transactions were included, giving >=2 matching messages.
13385            let ws_start = avail_block.saturating_sub(10);
13386            assert_ws_endpoint(api_port, &format!("availability/stream/leaves/{ws_start}")).await?;
13387            assert_ws_endpoint(api_port, &format!("availability/stream/headers/{ws_start}"))
13388                .await?;
13389            assert_ws_endpoint(api_port, &format!("availability/stream/blocks/{ws_start}")).await?;
13390            assert_ws_endpoint(
13391                api_port,
13392                &format!("availability/stream/payloads/{ws_start}"),
13393            )
13394            .await?;
13395            assert_ws_endpoint(
13396                api_port,
13397                &format!("availability/stream/vid/common/{ws_start}"),
13398            )
13399            .await?;
13400            assert_ws_endpoint(
13401                api_port,
13402                &format!("availability/stream/transactions/{ws_start}"),
13403            )
13404            .await?;
13405            // Namespace-filtered streams: start at avail_block; two transactions were
13406            // submitted so the stream produces ≥2 messages.
13407            assert_ws_endpoint(
13408                api_port,
13409                &format!("availability/stream/transactions/{avail_block}/namespace/{avail_ns}"),
13410            )
13411            .await?;
13412            assert_ws_endpoint(
13413                api_port,
13414                &format!("availability/stream/blocks/{avail_block}/namespace/{avail_ns}"),
13415            )
13416            .await?;
13417
13418            // Our clients default to `Accept: application/octet-stream`, so the server must
13419            // emit `Message::Binary` (VBS-encoded) frames on that path. Verify it does so
13420            // on a representative stream.
13421            assert_ws_endpoint_binary(api_port, &format!("availability/stream/leaves/{ws_start}"))
13422                .await?;
13423
13424            // Merklized state endpoints (block-state and fee-state). Wait for
13425            // the backend to have indexed the snapshot we'll query.
13426            wait_until_block_height(&client, "block-state/block-height", avail_block).await;
13427            wait_until_block_height(&client, "fee-state/block-height", avail_block).await;
13428
13429            // block-state/block-height and fee-state/block-height (latest
13430            // height for which merklized state is available).
13431            assert_json_endpoint(&http, api_port, "block-state/block-height").await?;
13432            assert_json_endpoint(&http, api_port, "fee-state/block-height").await?;
13433
13434            // block-state path by height: the merkle tree at height H
13435            // contains the headers of blocks [0, H), so a valid key is H-1.
13436            assert_json_endpoint(
13437                &http,
13438                api_port,
13439                &format!(
13440                    "block-state/{avail_block}/{}",
13441                    avail_block.saturating_sub(1)
13442                ),
13443            )
13444            .await?;
13445
13446            // block-state path by commit. Use the tree commitment from
13447            // the header at avail_block.
13448            let block_mt_commit = avail_header.block_merkle_tree_root().to_string();
13449            assert_json_endpoint(
13450                &http,
13451                api_port,
13452                &format!(
13453                    "block-state/commit/{block_mt_commit}/{}",
13454                    avail_block.saturating_sub(1)
13455                ),
13456            )
13457            .await?;
13458
13459            // fee-state path by height for a known fee account (sampled above while
13460            // consensus was running), and fee-balance/latest for the same account.
13461            assert_json_endpoint(
13462                &http,
13463                api_port,
13464                &format!("fee-state/{avail_block}/{fee_account}"),
13465            )
13466            .await?;
13467            let fee_mt_commit = avail_header.fee_merkle_tree_root().to_string();
13468            assert_json_endpoint(
13469                &http,
13470                api_port,
13471                &format!("fee-state/commit/{fee_mt_commit}/{fee_account}"),
13472            )
13473            .await?;
13474            assert_json_endpoint(
13475                &http,
13476                api_port,
13477                &format!("fee-state/fee-balance/latest/{fee_account}"),
13478            )
13479            .await?;
13480
13481            // Status endpoints. Block height and success rate are stable since consensus is
13482            // stopped; time-since-last-decide and metrics vary by wall-clock so we only
13483            // check for a 2xx.
13484            assert_json_endpoint(&http, api_port, "status/block-height").await?;
13485            assert_json_endpoint(&http, api_port, "status/success-rate").await?;
13486            assert_endpoint_ok(&http, api_port, "status/time-since-last-decide").await?;
13487            assert_endpoint_ok(&http, api_port, "status/metrics").await?;
13488
13489            // Config endpoints. /runtime returns 404 because no PublicNodeConfig was
13490            // configured for this test.
13491            assert_json_endpoint(&http, api_port, "config/hotshot").await?;
13492            assert_json_endpoint(&http, api_port, "config/env").await?;
13493            assert_endpoint_status(&http, api_port, "config/runtime", 404).await?;
13494
13495            // Node endpoints.
13496            assert_json_endpoint(&http, api_port, "node/block-height").await?;
13497            assert_json_endpoint(&http, api_port, "node/transactions/count").await?;
13498            assert_json_endpoint(
13499                &http,
13500                api_port,
13501                &format!("node/transactions/count/{avail_block}"),
13502            )
13503            .await?;
13504            assert_json_endpoint(
13505                &http,
13506                api_port,
13507                &format!("node/transactions/count/0/{avail_block}"),
13508            )
13509            .await?;
13510            assert_json_endpoint(
13511                &http,
13512                api_port,
13513                &format!("node/transactions/count/namespace/{avail_ns}"),
13514            )
13515            .await?;
13516            assert_json_endpoint(
13517                &http,
13518                api_port,
13519                &format!("node/transactions/count/namespace/{avail_ns}/{avail_block}"),
13520            )
13521            .await?;
13522            assert_json_endpoint(
13523                &http,
13524                api_port,
13525                &format!("node/transactions/count/namespace/{avail_ns}/0/{avail_block}"),
13526            )
13527            .await?;
13528
13529            assert_json_endpoint(&http, api_port, "node/payloads/size").await?;
13530            assert_json_endpoint(&http, api_port, "node/payloads/total-size").await?;
13531            assert_json_endpoint(
13532                &http,
13533                api_port,
13534                &format!("node/payloads/size/{avail_block}"),
13535            )
13536            .await?;
13537            assert_json_endpoint(
13538                &http,
13539                api_port,
13540                &format!("node/payloads/size/0/{avail_block}"),
13541            )
13542            .await?;
13543            assert_json_endpoint(
13544                &http,
13545                api_port,
13546                &format!("node/payloads/size/namespace/{avail_ns}"),
13547            )
13548            .await?;
13549            assert_json_endpoint(
13550                &http,
13551                api_port,
13552                &format!("node/payloads/size/namespace/{avail_ns}/{avail_block}"),
13553            )
13554            .await?;
13555            assert_json_endpoint(
13556                &http,
13557                api_port,
13558                &format!("node/payloads/size/namespace/{avail_ns}/0/{avail_block}"),
13559            )
13560            .await?;
13561
13562            assert_json_endpoint(&http, api_port, &format!("node/vid/share/{avail_block}")).await?;
13563            assert_json_endpoint(
13564                &http,
13565                api_port,
13566                &format!("node/vid/share/hash/{block_hash}"),
13567            )
13568            .await?;
13569            assert_json_endpoint(
13570                &http,
13571                api_port,
13572                &format!("node/vid/share/payload-hash/{payload_hash}"),
13573            )
13574            .await?;
13575
13576            assert_json_endpoint(&http, api_port, "node/sync-status").await?;
13577            assert_json_endpoint(&http, api_port, "node/limits").await?;
13578
13579            // Header window: cover all three start variants (time, height, hash). `end` is
13580            // an exclusive Unix-second cutoff; using the block's own timestamp + 1 yields
13581            // a deterministic single-block window.
13582            let avail_ts = avail_header.timestamp();
13583            assert_json_endpoint(
13584                &http,
13585                api_port,
13586                &format!("node/header/window/{avail_ts}/{}", avail_ts + 1),
13587            )
13588            .await?;
13589            assert_json_endpoint(
13590                &http,
13591                api_port,
13592                &format!("node/header/window/from/{avail_block}/{}", avail_ts + 1),
13593            )
13594            .await?;
13595            assert_json_endpoint(
13596                &http,
13597                api_port,
13598                &format!("node/header/window/from/hash/{block_hash}/{}", avail_ts + 1),
13599            )
13600            .await?;
13601
13602            assert_json_endpoint(&http, api_port, "node/stake-table/current").await?;
13603            assert_json_endpoint(&http, api_port, "node/stake-table/1").await?;
13604            assert_json_endpoint(&http, api_port, "node/da-stake-table/current").await?;
13605            assert_json_endpoint(&http, api_port, "node/da-stake-table/1").await?;
13606
13607            assert_json_endpoint(&http, api_port, "node/validators/1").await?;
13608            assert_json_endpoint(&http, api_port, "node/all-validators/1/0/100").await?;
13609
13610            assert_json_endpoint(&http, api_port, "node/participation/proposal/current").await?;
13611            assert_json_endpoint(&http, api_port, "node/participation/proposal/1").await?;
13612            assert_json_endpoint(&http, api_port, "node/participation/vote/current").await?;
13613            assert_json_endpoint(&http, api_port, "node/participation/vote/1").await?;
13614
13615            assert_json_endpoint(&http, api_port, "node/block-reward").await?;
13616            assert_json_endpoint(&http, api_port, "node/block-reward/epoch/1").await?;
13617
13618            assert_json_endpoint(&http, api_port, "node/oldest-block").await?;
13619            assert_json_endpoint(&http, api_port, "node/oldest-leaf").await?;
13620
13621            // Catchup endpoints. View number and height for in-memory state aren't readily
13622            // available after stopping consensus, so we check error semantics on
13623            // intentionally invalid lookups and the deprecated routes.
13624            let decided_view = decided_leaf.view_number().u64();
13625            assert_json_endpoint(
13626                &http,
13627                api_port,
13628                &format!("catchup/{height}/{decided_view}/blocks"),
13629            )
13630            .await?;
13631            // chain-config: a malformed TaggedBase64 commitment (bad checksum) parses-fails
13632            // on the request path and yields 400.
13633            assert_endpoint_status(
13634                &http,
13635                api_port,
13636                "catchup/chain-config/CHAINCONFIG~AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
13637                400,
13638            )
13639            .await?;
13640            // leafchain: undecided height returns 404 from both.
13641            assert_endpoint_status(&http, api_port, "catchup/999999/leafchain", 404).await?;
13642            // cert2: missing cert returns 404.
13643            assert_endpoint_status(&http, api_port, "catchup/999999/cert2", 404).await?;
13644            // Deprecated catchup routes still respond 404.
13645            assert_endpoint_status(&http, api_port, "catchup/1/reward-amounts/100/0", 404).await?;
13646
13647            // Production peer-catchup posts VBS-binary bodies via http-client.
13648            // Exercise the bulk-account POST endpoints in that exact wire format so any
13649            // regression to "JSON-only body" is caught here.
13650            // Reuse the account sampled above; `validated_state.fee_merkle_tree` was
13651            // captured before the fee-paying blocks and can be empty.
13652            assert_post_binary(
13653                &http,
13654                api_port,
13655                &format!("catchup/{height}/{decided_view}/accounts"),
13656                &vec![fee_account],
13657            )
13658            .await?;
13659            // reward-accounts V1 takes a Vec<RewardAccountV1>. We send empty since the V2
13660            // tree may not have V1-shaped entries in this test, but the wire format is what
13661            // we're validating.
13662            assert_post_binary(
13663                &http,
13664                api_port,
13665                &format!("catchup/{height}/{decided_view}/reward-accounts"),
13666                &Vec::<espresso_types::v0_3::RewardAccountV1>::new(),
13667            )
13668            .await?;
13669
13670            // State signature: missing heights should 404.
13671            assert_endpoint_status(&http, api_port, "state-signature/block/999999", 404).await?;
13672
13673            // Error bodies for endpoints that fail via `ApiError` must keep the
13674            // `{"Custom":{"message","status"}}` JSON envelope that existing clients parse.
13675            // Availability endpoints (`availability/leaf/...`, etc.) are excluded because
13676            // they use per-endpoint error variants like `{"FetchLeaf":{...}}`; their status
13677            // codes are still checked by `assert_endpoint_status` above.
13678            assert_error_body(&http, api_port, "catchup/999999/cert2", 404).await?;
13679            assert_error_body(&http, api_port, "catchup/999999/leafchain", 404).await?;
13680            assert_error_body(&http, api_port, "state-signature/block/999999", 404).await?;
13681
13682            // Explorer endpoints.
13683            assert_json_endpoint(&http, api_port, "explorer/explorer-summary").await?;
13684            assert_json_endpoint(&http, api_port, &format!("explorer/block/{avail_block}")).await?;
13685            assert_json_endpoint(
13686                &http,
13687                api_port,
13688                &format!("explorer/block/hash/{block_hash}"),
13689            )
13690            .await?;
13691            assert_json_endpoint(&http, api_port, "explorer/blocks/latest/10").await?;
13692            assert_json_endpoint(
13693                &http,
13694                api_port,
13695                &format!("explorer/blocks/{avail_block}/10"),
13696            )
13697            .await?;
13698            assert_json_endpoint(&http, api_port, "explorer/transactions/latest/10").await?;
13699
13700            // Explorer errors keep their status: missing objects are 404 and unsupported
13701            // search tags are 400, while a search that finds nothing is an empty result.
13702            assert_error_body(&http, api_port, "explorer/block/999999", 404).await?;
13703            assert_error_body(&http, api_port, "explorer/transaction/999999/0", 404).await?;
13704            assert_error_body(
13705                &http,
13706                api_port,
13707                &format!("explorer/search/{leaf_hash}"),
13708                400,
13709            )
13710            .await?;
13711            let unknown_block = tagged_base64::TaggedBase64::new("BLOCK", &[0; 32])?;
13712            let search: serde_json::Value = http
13713                .get(format!(
13714                    "http://localhost:{api_port}/v1/explorer/search/{unknown_block}"
13715                ))
13716                .send()
13717                .await?
13718                .error_for_status()?
13719                .json()
13720                .await?;
13721            assert_eq!(
13722                search["search_results"],
13723                serde_json::json!({"blocks": [], "transactions": []})
13724            );
13725
13726            // Light-client endpoints. Use the same block we used for availability tests.
13727            assert_json_endpoint(&http, api_port, &format!("light-client/leaf/{avail_block}"))
13728                .await?;
13729            assert_json_endpoint(
13730                &http,
13731                api_port,
13732                &format!("light-client/leaf/hash/{leaf_hash}"),
13733            )
13734            .await?;
13735            assert_json_endpoint(
13736                &http,
13737                api_port,
13738                &format!("light-client/payload/{avail_block}"),
13739            )
13740            .await?;
13741            assert_json_endpoint(
13742                &http,
13743                api_port,
13744                &format!("light-client/payload/{avail_block}/{}", avail_block + 1),
13745            )
13746            .await?;
13747            assert_json_endpoint(
13748                &http,
13749                api_port,
13750                &format!(
13751                    "light-client/namespace/{avail_block}/{}",
13752                    u64::from(avail_ns)
13753                ),
13754            )
13755            .await?;
13756            assert_json_endpoint(
13757                &http,
13758                api_port,
13759                &format!(
13760                    "light-client/namespace/{avail_block}/{}/{}",
13761                    avail_block + 1,
13762                    u64::from(avail_ns)
13763                ),
13764            )
13765            .await?;
13766
13767            // Regression: an oversized range on the plural namespaces route must return
13768            // 400 Bad Request (the status carried by the query-service error), not 500.
13769            let encoded_ns = tagged_base64::TaggedBase64::new(
13770                ::light_client::client::NAMESPACES_PARAM_TAG,
13771                &serde_json::to_vec(&vec![u64::from(avail_ns)])?,
13772            )?;
13773            assert_endpoint_status(
13774                &http,
13775                api_port,
13776                &format!(
13777                    "light-client/namespaces/{avail_block}/{}/{encoded_ns}",
13778                    avail_block + 200
13779                ),
13780                400,
13781            )
13782            .await?;
13783
13784            // Token endpoints.
13785            assert_json_endpoint(&http, api_port, "token/total-minted-supply").await?;
13786            assert_json_endpoint(&http, api_port, "token/circulating-supply").await?;
13787            assert_json_endpoint(&http, api_port, "token/circulating-supply-ethereum").await?;
13788            assert_json_endpoint(&http, api_port, "token/total-issued-supply").await?;
13789            assert_json_endpoint(&http, api_port, "token/total-reward-distributed").await?;
13790
13791            // HTTP status codes for common failure cases that clients depend on.
13792
13793            // Requesting a leaf far ahead of the chain tip times out and returns
13794            // 404 Not Found.
13795            assert_endpoint_status(&http, api_port, "availability/leaf/999999", 404).await?;
13796
13797            // Requesting a block range that exceeds the per-request limit
13798            // returns 400 Bad Request.
13799            assert_endpoint_status(
13800                &http,
13801                api_port,
13802                &format!("availability/block/{avail_block}/{}", avail_block + 200),
13803                400,
13804            )
13805            .await?;
13806
13807            // Requesting a namespace proof range that exceeds the limit also
13808            // returns 400 Bad Request.
13809            assert_endpoint_status(
13810                &http,
13811                api_port,
13812                &format!(
13813                    "availability/block/{avail_block}/{}/namespace/{avail_ns}",
13814                    avail_block + 200
13815                ),
13816                400,
13817            )
13818            .await?;
13819
13820            anyhow::Ok(())
13821        };
13822
13823        // `block_on` polls the future on the *calling* thread. The default test thread stack is
13824        // 2 MiB on Linux, which isn't enough for this test's large async state machine. We spawn
13825        // a fresh thread with 32 MiB and run the tokio runtime there instead.
13826        std::thread::Builder::new()
13827            .stack_size(32 * 1024 * 1024)
13828            .spawn(move || {
13829                tokio::runtime::Builder::new_multi_thread()
13830                    .enable_all()
13831                    .build()
13832                    .unwrap()
13833                    .block_on(test)
13834                    .unwrap()
13835            })
13836            .unwrap()
13837            .join()
13838            .unwrap()
13839    }
13840
13841    #[apply(sql_data_sources)]
13842    async fn test_all_validators_endpoint<D: TestableSqlDataSource>(
13843        _d: PhantomData<D>,
13844    ) -> anyhow::Result<()> {
13845        const EPOCH_HEIGHT: u64 = 20;
13846
13847        let network_config = TestConfigBuilder::default()
13848            .epoch_height(EPOCH_HEIGHT)
13849            .build();
13850
13851        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
13852
13853        const NUM_NODES: usize = 5;
13854
13855        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
13856
13857        let config = TestNetworkConfigBuilder::with_num_nodes()
13858            .api_config(D::options(&storage[0], Options::with_port(api_port)))
13859            .network_config(network_config)
13860            .persistences(persistence.clone())
13861            .build();
13862
13863        let network = TestNetwork::new(config, TEST_UPGRADE).await;
13864        let client: Client<ClientErr, SequencerApiVersion> =
13865            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
13866
13867        let err = client
13868            .get::<Vec<RegisteredValidator<PubKey>>>("node/all-validators/1/0/1001")
13869            .header("Accept", "application/json")
13870            .send()
13871            .await
13872            .unwrap_err();
13873
13874        assert_matches!(err, ClientErr { status, message} if
13875                status == StatusCode::BAD_REQUEST
13876                && message.contains("Limit cannot be greater than 1000")
13877        );
13878
13879        // Wait for the chain to progress beyond epoch 3
13880        let mut events = network.peers[0].event_stream();
13881        wait_for_epochs(&mut events, EPOCH_HEIGHT, 3).await;
13882
13883        // Verify that there are no validators for epoch # 1 and epoch # 2
13884        {
13885            client
13886                .get::<Vec<RegisteredValidator<PubKey>>>("node/all-validators/1/0/100")
13887                .send()
13888                .await
13889                .unwrap()
13890                .is_empty();
13891
13892            client
13893                .get::<Vec<RegisteredValidator<PubKey>>>("node/all-validators/2/0/100")
13894                .send()
13895                .await
13896                .unwrap()
13897                .is_empty();
13898        }
13899
13900        // Get the epoch # 3 validators
13901        let validators = client
13902            .get::<Vec<RegisteredValidator<PubKey>>>("node/all-validators/3/0/100")
13903            .send()
13904            .await
13905            .expect("validators");
13906
13907        assert!(!validators.is_empty());
13908
13909        Ok(())
13910    }
13911
13912    #[apply(sql_data_sources)]
13913    async fn test_reward_accounts_catchup_endpoint<D: TestableSqlDataSource>(
13914        _d: PhantomData<D>,
13915    ) -> anyhow::Result<()> {
13916        const EPOCH_HEIGHT: u64 = 10;
13917        const NUM_NODES: usize = 3;
13918
13919        let network_config = TestConfigBuilder::default()
13920            .epoch_height(EPOCH_HEIGHT)
13921            .build();
13922
13923        let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
13924        println!("API PORT = {api_port}");
13925
13926        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
13927
13928        let config = TestNetworkConfigBuilder::with_num_nodes()
13929            .api_config(D::options(
13930                &storage[0],
13931                Options::with_port(api_port).catchup(Default::default()),
13932            ))
13933            .network_config(network_config)
13934            .persistences(persistence.clone())
13935            .build();
13936
13937        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
13938
13939        let client: Client<ClientErr, StaticVersion<0, 1>> =
13940            Client::new(format!("http://localhost:{api_port}").parse().unwrap());
13941
13942        client.connect(None).await;
13943
13944        let mut events = network.server.event_stream();
13945        wait_for_epochs(&mut events, EPOCH_HEIGHT, 3).await;
13946
13947        network.stop_consensus().await;
13948        // Reward trees are stored only at epoch boundaries, the only blocks that change them.
13949        let height = network.server.decided_leaf().await.height() / EPOCH_HEIGHT * EPOCH_HEIGHT;
13950        wait_until_block_height(&client, "reward-state-v2/block-height", height).await;
13951
13952        let err = client
13953            .get::<Vec<(RewardAccountV2, RewardAmount)>>(&format!(
13954                "reward-state-v2/reward-amounts/{height}/0/10001"
13955            ))
13956            .send()
13957            .await
13958            .unwrap_err();
13959
13960        assert_matches!(err, ClientErr { status, .. } if
13961            status == StatusCode::BAD_REQUEST
13962
13963        );
13964
13965        let mut expected: Vec<_> = network
13966            .server
13967            .decided_state()
13968            .await
13969            .unwrap()
13970            .reward_merkle_tree_v2
13971            .iter()
13972            .map(|(addr, amt)| (*addr, *amt))
13973            .collect();
13974        // Results are sorted by account address descending
13975        expected.sort_by_key(|(acct, _)| std::cmp::Reverse(*acct));
13976
13977        tracing::info!("expected accounts = {expected:?}");
13978        let limit = expected.len().min(10_000) as u64;
13979        let offset = 0u64;
13980        let expected: Vec<_> = expected.into_iter().take(limit as usize).collect();
13981
13982        let res = client
13983            .get::<Vec<(RewardAccountV2, RewardAmount)>>(&format!(
13984                "reward-state-v2/reward-amounts/{height}/{offset}/{limit}"
13985            ))
13986            .send()
13987            .await
13988            .unwrap();
13989
13990        assert_eq!(res, expected);
13991
13992        Ok(())
13993    }
13994
13995    #[apply(sql_data_sources)]
13996    async fn test_namespace_query_compat<D: TestableSqlDataSource>(_d: PhantomData<D>) {
13997        // Number of nodes running in the test network.
13998        const NUM_NODES: usize = 5;
13999
14000        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
14001        let url: Url = format!("http://localhost:{port}").parse().unwrap();
14002
14003        // At 0.6 the query service gets decided payloads and VID from the DA proposals and VID
14004        // shares in consensus storage, so the nodes need real persistence.
14005        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
14006
14007        let test_config = TestConfigBuilder::default().build();
14008        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
14009            .api_config(D::options(&storage[0], Options::with_port(port)))
14010            .persistences(persistence)
14011            .catchups(std::array::from_fn(|_| {
14012                StatePeers::<SequencerApiVersion>::from_urls(
14013                    vec![url.clone()],
14014                    Default::default(),
14015                    Duration::from_secs(2),
14016                    &NoMetrics,
14017                )
14018            }))
14019            .network_config(test_config)
14020            .build();
14021
14022        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
14023        let mut events = network.server.event_stream();
14024
14025        // Submit a transaction.
14026        let ns = NamespaceId::from(10_000u64);
14027        let tx = Transaction::new(ns, vec![1, 2, 3]);
14028        network.server.submit_transaction(tx.clone()).await.unwrap();
14029        let block = wait_for_decide_on_handle(&mut events, &tx).await.0;
14030
14031        // Check namespace proof queries.
14032        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
14033        client.connect(None).await;
14034
14035        let (header, common): (Header, VidCommonQueryData<SeqTypes>) = try_join!(
14036            client.get(&format!("availability/header/{block}")).send(),
14037            client
14038                .get(&format!("availability/vid/common/{block}"))
14039                .send()
14040        )
14041        .unwrap();
14042
14043        // The latest version of the API (whether we specifically ask for v1 or let the redirect
14044        // occur) will give us a namespace proof no matter which VID version is in use.
14045        for api_ver in ["/v1", ""] {
14046            tracing::info!("test namespace API version: {api_ver}");
14047
14048            let ns_proof: NamespaceProofQueryData = client
14049                .get(&format!(
14050                    "{api_ver}/availability/block/{block}/namespace/{ns}"
14051                ))
14052                .send()
14053                .await
14054                .unwrap();
14055            let proof = ns_proof.proof.as_ref().unwrap();
14056            assert!(matches!(proof, NsProof::V2(..)));
14057            let (txs, ns_from_proof) = proof
14058                .verify(
14059                    header.ns_table(),
14060                    &header.payload_commitment(),
14061                    common.common(),
14062                )
14063                .unwrap();
14064            assert_eq!(ns_from_proof, ns);
14065            assert_eq!(txs, ns_proof.transactions);
14066            assert_eq!(txs, std::slice::from_ref(&tx));
14067
14068            // Test range endpoint.
14069            let ns_proofs: Vec<NamespaceProofQueryData> = client
14070                .get(&format!(
14071                    "{api_ver}/availability/block/{}/{}/namespace/{ns}",
14072                    block,
14073                    block + 1
14074                ))
14075                .send()
14076                .await
14077                .unwrap();
14078            assert_eq!(&ns_proofs, std::slice::from_ref(&ns_proof));
14079
14080            // Any API version can correctly tell us that the namespace does not exist.
14081            let ns_proof: NamespaceProofQueryData = client
14082                .get(&format!(
14083                    "{api_ver}/availability/block/{}/namespace/{ns}",
14084                    block - 1
14085                ))
14086                .send()
14087                .await
14088                .unwrap();
14089            assert_eq!(ns_proof.proof, None);
14090            assert_eq!(ns_proof.transactions, vec![]);
14091
14092            // Test streaming.
14093            let mut proofs = client
14094                .socket(&format!(
14095                    "{api_ver}/availability/stream/blocks/0/namespace/{ns}"
14096                ))
14097                .subscribe()
14098                .await
14099                .unwrap();
14100            for i in 0.. {
14101                tracing::info!(i, "stream proof");
14102                let proof: NamespaceProofQueryData = proofs.next().await.unwrap().unwrap();
14103                if proof.proof.is_none() {
14104                    tracing::info!("waiting for non-trivial proof from stream");
14105                    continue;
14106                }
14107                assert_eq!(&proof.transactions, std::slice::from_ref(&tx));
14108                break;
14109            }
14110        }
14111
14112        network.server.shut_down().await;
14113    }
14114
14115    /// Rollups still fetch their namespaces from mainnet blocks before 0.6, proven with ADVZ
14116    /// before 0.3 and AvidM after.
14117    #[rstest]
14118    #[case::v2_v3("v2-v3")]
14119    #[case::v4("v4")]
14120    #[case::v5("v5")]
14121    #[test_log::test(tokio::test(flavor = "multi_thread"))]
14122    async fn test_legacy_namespace_proofs<D: TestableSqlDataSource>(
14123        #[values(PhantomData::<Postgres>, PhantomData::<Sqlite>)] _d: PhantomData<D>,
14124        #[case] chain: &str,
14125    ) -> anyhow::Result<()> {
14126        let chain = LegacyChain::load(chain)?;
14127        let replay = chain.replay::<D>().await?;
14128        let versions = chain.blocks[1..]
14129            .iter()
14130            .map(|block| block.leaf.header().version())
14131            .collect::<HashSet<_>>();
14132        // Proving every block of the 1001-block upgrade chain takes too long.
14133        let sampled = versions.iter().flat_map(|version| {
14134            chain
14135                .blocks
14136                .iter()
14137                .filter(move |block| block.leaf.header().version() == *version)
14138                .take(25)
14139        });
14140        let mut proven = HashSet::new();
14141        for recorded in sampled {
14142            let header = recorded.leaf.header();
14143            let height = header.height();
14144            for ns in [101u64, 102].map(NamespaceId::from) {
14145                let res: NamespaceProofQueryData = replay
14146                    .client
14147                    .get(&format!("availability/block/{height}/namespace/{ns}"))
14148                    .send()
14149                    .await?;
14150                let Some(proof) = &res.proof else {
14151                    ensure!(
14152                        header.ns_table().find_ns_id(&ns).is_none() && res.transactions.is_empty(),
14153                        "no proof of namespace {ns} in block {height}"
14154                    );
14155                    continue;
14156                };
14157                assert_ns_proof_matches_version(proof, header.version());
14158                let (txs, proven_ns) = proof
14159                    .verify(
14160                        header.ns_table(),
14161                        &header.payload_commitment(),
14162                        recorded.vid_common.common(),
14163                    )
14164                    .with_context(|| format!("namespace {ns} of block {height}"))?;
14165                ensure!(
14166                    proven_ns == ns && !txs.is_empty() && txs == res.transactions,
14167                    "namespace {ns} of block {height} proves its transactions"
14168                );
14169                proven.insert(header.version());
14170            }
14171
14172            let cert2 = replay
14173                .client
14174                .get::<serde_json::Value>(&format!("availability/cert2/{height}"))
14175                .send()
14176                .await;
14177            ensure!(
14178                matches!(&cert2, Err(err) if err.status == StatusCode::NOT_FOUND),
14179                "legacy block {height} has a cert2: {cert2:?}"
14180            );
14181        }
14182        ensure!(proven == versions, "proved {proven:?} of {versions:?}");
14183        Ok(())
14184    }
14185
14186    /// ADVZ proves namespaces before 0.3, AvidM until 0.6 and AvidmGf2 from then on.
14187    fn assert_ns_proof_matches_version(proof: &NsProof, version: Version) {
14188        match proof {
14189            NsProof::V0(..) => assert!(version < EPOCH_VERSION, "{version}"),
14190            NsProof::V1(..) => assert!(
14191                (EPOCH_VERSION..NEW_PROTOCOL_VERSION).contains(&version),
14192                "{version}"
14193            ),
14194            NsProof::V2(..) => assert!(version >= NEW_PROTOCOL_VERSION, "{version}"),
14195            NsProof::V1IncorrectEncoding(..) => panic!("honest blocks are encoded correctly"),
14196        }
14197    }
14198
14199    /// Verify the light client leaf, header, and payload proofs at each height
14200    /// against the ground truth captured from the availability streams.
14201    ///
14202    /// Only checks that proofs verify, not which `FinalityProof` variant they
14203    /// use
14204    async fn check_light_client_proofs(
14205        client: &Client<ClientErr, StaticVersion<0, 1>>,
14206        actual_leaves: &[LeafQueryData<SeqTypes>],
14207        actual_blocks: &[BlockQueryData<SeqTypes>],
14208        heights: impl IntoIterator<Item = u64>,
14209        epoch_height: u64,
14210    ) {
14211        let quorum = EpochChangeQuorum::new(epoch_height);
14212        for i in heights {
14213            let leaf = &actual_leaves[i as usize];
14214            let block = &actual_blocks[i as usize];
14215            let version = leaf.header().version();
14216            tracing::info!(i, %version, "check light client proofs");
14217
14218            // Get the same leaf proof by various IDs.
14219            let proofs = try_join_all(
14220                [
14221                    format!("light-client/leaf/{i}"),
14222                    format!("light-client/leaf/hash/{}", leaf.hash()),
14223                    format!("light-client/leaf/block-hash/{}", leaf.block_hash()),
14224                ]
14225                .into_iter()
14226                .map(|path| async move {
14227                    tracing::info!(i, path, "fetch leaf proof");
14228                    let proof = client.get::<LeafProof>(&path).send().await?;
14229                    Ok::<_, anyhow::Error>((path, proof))
14230                }),
14231            )
14232            .await
14233            .unwrap();
14234
14235            // Check proofs against the expected leaf.
14236            for (path, proof) in proofs {
14237                tracing::info!(i, path, "check leaf proof");
14238                assert_eq!(
14239                    proof
14240                        .verify(LeafProofHint::Quorum(&quorum))
14241                        .await
14242                        .unwrap_or_else(|err| panic!("{path}: proof failed to verify: {err:#}")),
14243                    *leaf
14244                );
14245            }
14246
14247            // Get the corresponding header.
14248            let root_height = i + 1;
14249            let root = actual_leaves[root_height as usize].header();
14250            let proofs = try_join_all(
14251                [
14252                    format!("light-client/header/{root_height}/{i}"),
14253                    format!(
14254                        "light-client/header/{root_height}/hash/{}",
14255                        leaf.block_hash()
14256                    ),
14257                ]
14258                .into_iter()
14259                .map(|path| async move {
14260                    tracing::info!(i, path, "fetch header proof");
14261                    let proof = client.get::<HeaderProof>(&path).send().await?;
14262                    Ok::<_, anyhow::Error>((path, proof))
14263                }),
14264            )
14265            .await
14266            .unwrap();
14267            for (path, proof) in proofs {
14268                tracing::info!(i, path, "check header proof");
14269                assert_eq!(
14270                    proof.verify_ref(root.block_merkle_tree_root()).unwrap(),
14271                    leaf.header()
14272                );
14273            }
14274
14275            // Get the corresponding payload.
14276            let proof = client
14277                .get::<PayloadProof>(&format!("light-client/payload/{i}"))
14278                .send()
14279                .await
14280                .unwrap();
14281            assert_eq!(proof.verify(leaf.header()).unwrap(), *block.payload());
14282        }
14283    }
14284
14285    /// Check the light client stake table endpoint: replaying `first_epoch + 2`
14286    /// reproduces the validator set loaded from storage, and an earlier epoch
14287    /// is a `BAD_REQUEST`.
14288    async fn check_light_client_stake_table(
14289        client: &Client<ClientErr, StaticVersion<0, 1>>,
14290        storage: &impl SequencerPersistence,
14291        first_epoch: EpochNumber,
14292    ) {
14293        let events: Vec<StakeTableEvent> = client
14294            .get(&format!("light-client/stake-table/{}", first_epoch + 2))
14295            .send()
14296            .await
14297            .unwrap();
14298        let mut state_from_events = StakeTableState::default();
14299        for event in events {
14300            state_from_events.apply_event(event).unwrap().unwrap();
14301        }
14302        assert_eq!(
14303            state_from_events.into_validators(),
14304            storage
14305                .load_all_validators(first_epoch + 2, 0, 1_000_000)
14306                .await
14307                .unwrap()
14308                .into_iter()
14309                .map(|v| (v.account, v))
14310                .collect::<RegisteredValidatorMap>()
14311        );
14312
14313        // Querying for a stake table before the first real epoch is an error.
14314        let err = client
14315            .get::<Vec<StakeTableEvent>>(&format!("light-client/stake-table/{}", first_epoch + 1))
14316            .send()
14317            .await
14318            .unwrap_err();
14319        assert_eq!(err.status, StatusCode::BAD_REQUEST);
14320    }
14321
14322    #[apply(sql_data_sources)]
14323    async fn test_light_client_completeness<D: TestableSqlDataSource>(_d: PhantomData<D>) {
14324        // Run through two epoch changes, then check that we are able to get a correct light client
14325        // proof for every finalized leaf.
14326
14327        const NUM_NODES: usize = 1;
14328        const EPOCH_HEIGHT: u64 = 50;
14329
14330        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
14331        let url: Url = format!("http://localhost:{port}").parse().unwrap();
14332
14333        let test_config = TestConfigBuilder::default()
14334            .epoch_height(EPOCH_HEIGHT)
14335            .epoch_start_block(0)
14336            // No transactions here, so this sets the seconds per block, and the
14337            // test is bound by block count.
14338            .builder_timeout(Duration::from_millis(250))
14339            .build();
14340
14341        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
14342
14343        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
14344            .api_config(D::options(&storage[0], Options::with_port(port)))
14345            .persistences(persistence.clone())
14346            .catchups(std::array::from_fn(|_| {
14347                StatePeers::<SequencerApiVersion>::from_urls(
14348                    vec![url.clone()],
14349                    Default::default(),
14350                    Duration::from_secs(2),
14351                    &NoMetrics,
14352                )
14353            }))
14354            .network_config(test_config)
14355            .build();
14356
14357        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
14358        let client: Client<ClientErr, StaticVersion<0, 1>> = Client::new(url);
14359        client.connect(None).await;
14360
14361        // Get a leaf stream so that we can wait for various events. Also keep track of each leaf
14362        // yielded, which we can use as ground truth later in the test.
14363        let mut actual_leaves = vec![];
14364        let mut actual_blocks = vec![];
14365        let mut leaves = client
14366            .socket("availability/stream/leaves/0")
14367            .subscribe::<LeafQueryData<SeqTypes>>()
14368            .await
14369            .unwrap()
14370            .zip(
14371                client
14372                    .socket("availability/stream/blocks/0")
14373                    .subscribe::<BlockQueryData<SeqTypes>>()
14374                    .await
14375                    .unwrap(),
14376            )
14377            .map(|(leaf, block)| {
14378                let leaf = leaf.unwrap();
14379                let block = block.unwrap();
14380                actual_leaves.push(leaf.clone());
14381                actual_blocks.push(block);
14382                leaf
14383            });
14384
14385        let first_epoch = {
14386            let leaf: LeafQueryData<SeqTypes> = leaves.next().await.unwrap();
14387            leaf.leaf().epoch(EPOCH_HEIGHT).unwrap()
14388        };
14389
14390        // Wait for two epoch changes (so we get to the first epoch that actually uses the stake
14391        // table).
14392        let mut epoch_heights = [0; 2];
14393        for (i, epoch_height) in epoch_heights.iter_mut().enumerate() {
14394            let desired_epoch = first_epoch + (i as u64) + 1;
14395            *epoch_height = loop {
14396                let leaf = leaves.next().await.unwrap();
14397                let epoch = leaf.leaf().epoch(EPOCH_HEIGHT).unwrap();
14398                if epoch > desired_epoch {
14399                    tracing::info!(
14400                        height = leaf.height(),
14401                        ?desired_epoch,
14402                        ?epoch,
14403                        "changed epoch"
14404                    );
14405                    break leaf.height();
14406                }
14407                tracing::info!(
14408                    ?desired_epoch,
14409                    height = leaf.header().height(),
14410                    view = ?leaf.leaf().view_number(),
14411                    "waiting for epoch change"
14412                );
14413            };
14414        }
14415
14416        // Wait a few more blocks.
14417        let max_block = epoch_heights[1] + 1;
14418        loop {
14419            let leaf = leaves.next().await.unwrap();
14420            if leaf.height() > max_block {
14421                break;
14422            }
14423            tracing::info!(max_block, height = leaf.height(), "waiting for block");
14424        }
14425
14426        // Stop consensus. All the blocks we are going to query have already been produced.
14427        // Continuing to run consensus would just waste resources while we check stuff.
14428        network.stop_consensus().await;
14429
14430        // Check light client. Querying every single block is too slow, so we'll check a few blocks
14431        // around various critical points:
14432        let heights =
14433        // * The first few blocks, including genesis
14434            (0..=1)
14435        // * A few blocks just before and after the first epoch change
14436            .chain(epoch_heights[0]-1..=epoch_heights[0] + 1)
14437        // * A few blocks just before and after the stake table comes into effect
14438            .chain(epoch_heights[1]-1..=max_block);
14439
14440        check_light_client_proofs(
14441            &client,
14442            &actual_leaves,
14443            &actual_blocks,
14444            heights,
14445            EPOCH_HEIGHT,
14446        )
14447        .await;
14448        check_light_client_stake_table(&client, &*network.server.persistence(), first_epoch).await;
14449    }
14450
14451    /// Light clients still prove mainnet's leaves from before its 0.3 upgrade, across it and across
14452    /// the first epoch on the stake table, all produced by legacy consensus.
14453    #[apply(sql_data_sources)]
14454    async fn test_legacy_light_client_completeness<D: TestableSqlDataSource>(
14455        _d: PhantomData<D>,
14456    ) -> anyhow::Result<()> {
14457        let chain = LegacyChain::load("v2-v3")?;
14458        let replay = chain.replay::<D>().await?;
14459        let epoch_height = chain.epoch_height();
14460        let leaves: Vec<_> = chain.blocks.iter().map(|b| b.leaf.clone()).collect();
14461        let blocks: Vec<_> = chain.blocks.iter().map(|b| b.block.clone()).collect();
14462
14463        let upgraded = leaves
14464            .iter()
14465            .find(|leaf| leaf.header().version() >= EPOCH_VERSION)
14466            .context("the chain upgrades to 0.3")?;
14467        let upgrade_height = upgraded.height();
14468        let first_epoch = upgraded.leaf().epoch(epoch_height).unwrap();
14469        let epoch_change = |epoch: EpochNumber| {
14470            leaves
14471                .iter()
14472                .find(|leaf| leaf.leaf().epoch(epoch_height).is_some_and(|e| e > epoch))
14473                .map(|leaf| leaf.height())
14474                .context("the chain changes epoch")
14475        };
14476        let epoch_heights = [
14477            epoch_change(first_epoch + 1)?,
14478            epoch_change(first_epoch + 2)?,
14479        ];
14480        let max_block = epoch_heights[1] + 1;
14481        ensure!(
14482            max_block < chain.tip(),
14483            "a header proof needs the block after it"
14484        );
14485
14486        let heights = (0..=1)
14487            .chain(upgrade_height - 1..=upgrade_height + 1)
14488            .chain(epoch_heights[0] - 1..=epoch_heights[0] + 1)
14489            .chain(epoch_heights[1] - 1..=max_block);
14490        check_light_client_proofs(&replay.client, &leaves, &blocks, heights, epoch_height).await;
14491        check_light_client_stake_table(&replay.client, &replay.persistence().await?, first_epoch)
14492            .await;
14493        Ok(())
14494    }
14495
14496    /// Test that `fetch_leaf` returns a leaf with exactly the requested block height.
14497    #[apply(sql_data_sources)]
14498    async fn test_fetch_leaf_returns_exact_height<D: TestableSqlDataSource>(
14499        _d: PhantomData<D>,
14500    ) -> anyhow::Result<()> {
14501        const EPOCH_HEIGHT: u64 = 10;
14502        const NUM_NODES: usize = 5;
14503        const TARGET_HEIGHT: u64 = EPOCH_HEIGHT * 3 + 2;
14504
14505        let network_config = TestConfigBuilder::default()
14506            .epoch_height(EPOCH_HEIGHT)
14507            .build();
14508
14509        let port = reserve_tcp_port().expect("No ports free for query service");
14510
14511        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
14512
14513        let catchup_peers = std::array::from_fn(|_| {
14514            StatePeers::<StaticVersion<0, 1>>::from_urls(
14515                vec![format!("http://localhost:{port}").parse().unwrap()],
14516                Default::default(),
14517                Duration::from_secs(2),
14518                &NoMetrics,
14519            )
14520        });
14521
14522        let config = TestNetworkConfigBuilder::with_num_nodes()
14523            .api_config(D::options(&storage[0], Options::with_port(port)))
14524            .network_config(network_config)
14525            .persistences(persistence)
14526            .catchups(catchup_peers)
14527            .build();
14528
14529        let network = TestNetwork::new(config, TEST_UPGRADE).await;
14530
14531        // Wait for chain to advance past our target height
14532        let height_client: Client<ClientErr, StaticVersion<0, 1>> =
14533            Client::new(format!("http://localhost:{port}").parse().unwrap());
14534        wait_until_block_height(&height_client, "node/block-height", TARGET_HEIGHT + 5).await;
14535
14536        let coordinator = network.server.node_state().coordinator;
14537
14538        // Use StatePeers to fetch the leaf at the exact target height
14539        let catchup = StatePeers::<StaticVersion<0, 1>>::from_urls(
14540            vec![format!("http://localhost:{port}").parse().unwrap()],
14541            Default::default(),
14542            Duration::from_secs(5),
14543            &NoMetrics,
14544        );
14545
14546        let leaf = catchup.fetch_leaf(coordinator, TARGET_HEIGHT).await?;
14547
14548        assert_eq!(
14549            leaf.height(),
14550            TARGET_HEIGHT,
14551            "fetch_leaf must return the leaf at exactly the requested height"
14552        );
14553
14554        Ok(())
14555    }
14556
14557    /// Start a network at V5 from genesis, restart ALL nodes just before an epoch transition block
14558    /// (`boundary - 3`), and confirm the chain keeps producing blocks afterward.
14559    #[apply(sql_data_sources)]
14560    async fn test_v5_restart_before_epoch_boundary<D: TestableSqlDataSource>(_d: PhantomData<D>) {
14561        const NUM_NODES: usize = 3;
14562        const EPOCH_HEIGHT: u64 = 10;
14563        // Rewards start in epoch 4. Restart 3 blocks before the boundary that closes epoch 4, so
14564        // the restarted network produces the boundary block (`4 * EPOCH_HEIGHT`) itself.
14565        const RESTART_EPOCH: u64 = 4;
14566        const RESTART_BOUNDARY: u64 = RESTART_EPOCH * EPOCH_HEIGHT;
14567        const RESTART_HEIGHT: u64 = RESTART_BOUNDARY - 3;
14568        // Blocks to produce after the restart before declaring success.
14569        const BLOCKS_AFTER_RESTART: u64 = 5;
14570
14571        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
14572
14573        // Slow empty-block production so we comfortably stop at the exact target height. On an idle
14574        // chain the empty-block time is ~`builder_timeout`; raise `next_view_timeout` above it so
14575        // the slow (but healthy) views aren't treated as failures.
14576        let network_config = TestConfigBuilder::<NUM_NODES>::default()
14577            .epoch_height(EPOCH_HEIGHT)
14578            .builder_timeout(Duration::from_secs(3))
14579            .next_view_timeout(Duration::from_secs(10))
14580            .build();
14581
14582        let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
14583
14584        let config = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
14585            .api_config(D::options(&storage[0], Options::with_port(port)))
14586            .persistences(persistence.clone())
14587            .network_config(network_config)
14588            .build();
14589
14590        let mut network = TestNetwork::new(config, TEST_UPGRADE).await;
14591
14592        // Watch the decide stream and stop as soon as `boundary - 3` is decided. Consuming the
14593        // stream (rather than polling `decided_leaf`) makes this independent of block timing: we
14594        // see every decided leaf and stop the network the moment the target height appears, so we
14595        // can never race past it regardless of how fast blocks are produced.
14596        {
14597            let mut events = network.server.event_stream();
14598            'wait: loop {
14599                let event = events
14600                    .next()
14601                    .await
14602                    .expect("event stream ended unexpectedly");
14603                let CoordinatorEvent::NewDecide {
14604                    leaf_infos: leaf_chain,
14605                    ..
14606                } = event
14607                else {
14608                    continue;
14609                };
14610                // `leaf_chain` is newest-first; once any decided leaf has reached the target
14611                // height, the chain is at or past it.
14612                for LeafInfo { leaf, .. } in leaf_chain.iter() {
14613                    if leaf.block_header().height() >= RESTART_HEIGHT {
14614                        break 'wait;
14615                    }
14616                }
14617            }
14618        }
14619
14620        let restart_height = network.server.decided_leaf().await.height();
14621        tracing::info!(
14622            restart_height,
14623            restart_epoch = RESTART_EPOCH,
14624            restart_boundary = RESTART_BOUNDARY,
14625            "restarting all nodes 3 blocks before an epoch boundary"
14626        );
14627
14628        // Clone the TestConfig before dropping the network so the anvil/L1/contracts stay alive.
14629        let saved_cfg = network.cfg.clone();
14630        // The genesis states carry the deployed stake table, so the rebuild deploys no new one.
14631        let genesis_states: [_; NUM_NODES] = std::array::from_fn(|i| network.genesis_state(i));
14632
14633        network.stop_consensus().await;
14634        drop(network);
14635
14636        // Rebuild reusing the same persistence so nodes resume from stored state.
14637        let port2 = reserve_tcp_port().expect("OS should have ephemeral ports available");
14638        let config2 = TestNetworkConfigBuilder::<NUM_NODES, _, _>::with_num_nodes()
14639            .api_config(D::options(&storage[0], Options::with_port(port2)))
14640            .persistences(persistence)
14641            .network_config(saved_cfg)
14642            .states(genesis_states)
14643            .build();
14644        let network2 = TestNetwork::new(config2, TEST_UPGRADE).await;
14645
14646        // The restarted network must keep advancing, including across the next epoch boundary.
14647        // Require BLOCKS_AFTER_RESTART new decides, using a lack-of-progress watchdog so a
14648        // healthy-but-slow chain still passes.
14649        let target_height = restart_height + BLOCKS_AFTER_RESTART;
14650        let stall_limit = 30; // 30 polls * 2s = 60s without a new decide => stalled
14651        let mut last_height = network2.server.decided_leaf().await.height();
14652        let mut stalled_polls = 0;
14653        while last_height < target_height {
14654            sleep(Duration::from_secs(2)).await;
14655            let height = network2.server.decided_leaf().await.height();
14656            if height > last_height {
14657                last_height = height;
14658                stalled_polls = 0;
14659            } else {
14660                stalled_polls += 1;
14661                if stalled_polls >= stall_limit {
14662                    panic!(
14663                        "chain stalled after restart 3 blocks before boundary {RESTART_BOUNDARY}: \
14664                         no new decide for 60s at height {last_height}, unable to produce/cross \
14665                         the epoch boundary (target height was {target_height})."
14666                    );
14667                }
14668            }
14669        }
14670    }
14671}