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;
98pub 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 #[derivative(Debug = "ignore")]
122 context: BoxLazy<Box<dyn ApiContext>>,
123
124 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 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 async fn get_stake_table_current(&self) -> anyhow::Result<StakeTableWithEpochNumber<SeqTypes>> {
193 self.as_ref().get_stake_table_current().await
194 }
195
196 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 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 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 async fn current_proposal_participation(&self) -> HashMap<PubKey, f64> {
227 self.as_ref().current_proposal_participation().await
228 }
229 async fn proposal_participation(&self, epoch: EpochNumber) -> HashMap<PubKey, f64> {
231 self.as_ref().proposal_participation(epoch).await
232 }
233 async fn current_vote_participation(&self) -> HashMap<PubKey, f64> {
235 self.as_ref().current_vote_participation().await
236 }
237 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 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 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 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 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 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 async fn current_proposal_participation(&self) -> HashMap<PubKey, f64> {
413 self.consensus()
414 .await
415 .current_proposal_participation()
416 .await
417 }
418
419 async fn proposal_participation(&self, epoch: EpochNumber) -> HashMap<PubKey, f64> {
421 self.consensus().await.proposal_participation(epoch).await
422 }
423
424 async fn current_vote_participation(&self) -> HashMap<PubKey, f64> {
426 self.consensus().await.current_vote_participation().await
427 }
428
429 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 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 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 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#[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 let cf = handle
706 .decided_state()
707 .await
708 .and_then(|state| state.chain_config.resolve());
709
710 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 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 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 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 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 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 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 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 self.inner().get_chain_config(commitment).await
843 }
844 async fn get_leaf_chain(&self, height: u64) -> anyhow::Result<Vec<Leaf2>> {
845 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 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 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 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 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 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 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 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 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 last_leaf = leaf;
1048 break;
1049 }
1050 last_leaf = leaf;
1051 }
1052 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)]
1183pub struct RewardMerkleTreeV2Data {
1185 pub balances: Vec<(RewardAccountV2, RewardAmount)>,
1186}
1187
1188impl TryInto<RewardMerkleTreeV2Data> for &RewardMerkleTreeV2 {
1189 type Error = anyhow::Error;
1190 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 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 let epoch_height = node_state
1256 .epoch_height
1257 .context("reward tree gc requires an epoch height")?;
1258 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 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 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 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 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 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 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
1604impl<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 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#[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 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 pub temp_dir: Option<TempDir>,
1876 pub contracts: Option<Contracts>,
1877 deferred: Vec<usize>,
1879 genesis_states: Vec<ValidatedState>,
1880 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 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 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 pub fn deferred_start(mut self, indices: &[usize]) -> Self {
2052 self.deferred_start = indices.to_vec();
2053 self
2054 }
2055
2056 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 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 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 ®istered,
2097 )
2098 .await
2099 }
2100
2101 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 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 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 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 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 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 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 pub fn genesis_state(&self, i: usize) -> ValidatedState {
2347 self.genesis_states[i].clone()
2348 }
2349
2350 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 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 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 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 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 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 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 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 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 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 pub fn committee_is(expected: HashSet<Address>) -> impl Fn(&AuthenticatedValidatorMap) -> bool {
2660 move |validators| validators.keys().copied().collect::<HashSet<_>>() == expected
2661 }
2662
2663 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(&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 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 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 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 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 assert!(success_rate.is_finite(), "{success_rate}");
2785 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 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_decide_on_handle(&mut events, &txn).await;
2850 }
2851
2852 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 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 client
2880 .get::<LCV3StateSignatureRequestBody>(&format!("state-signature/block/{height}"))
2881 .send()
2882 .await
2883 .unwrap();
2884 }
2885
2886 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 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 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 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 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 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 let ns_id = NamespaceId::from(42_u32);
3062 let txn = Transaction::new(ns_id, vec![1, 2, 3, 4]);
3063
3064 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 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 let block_height = wait_for_decide_on_handle(&mut events, &txn).await.0 as usize;
3091 tracing::info!(block_height, "transaction sequenced");
3092
3093 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 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 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 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 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 found_txn = true;
3180 }
3181 }
3182 }
3183 assert!(found_txn);
3184 assert!(found_empty_block);
3185
3186 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 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 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 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 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 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 let mut chain2 = chain1.split_off(2);
3346 chain2.remove(0);
3348
3349 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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[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 #[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 ("leaf/1/1".to_string(), reqwest::StatusCode::BAD_REQUEST),
3973 ("header/1/1".to_string(), reqwest::StatusCode::BAD_REQUEST),
3975 (
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 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 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 restart_from_genesis_and_decide(network, catchup, |info| info.delta.is_some()).await;
4042 }
4043
4044 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 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 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 #[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 #[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 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 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 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 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 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 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 tx.insert_block(&blocks[0]).await.unwrap();
4292 tx.insert_block(empty[0]).await.unwrap();
4293 tx.commit().await.unwrap();
4294
4295 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 let mut late_db = D::tmp_options(&dbs[LATE]);
4317 late_db.proactive_scan_interval = Some(Duration::from_secs(1));
4318 late_db.proactive_fetch_timeout = Some(Duration::from_secs(5));
4321 late_db.sync_status_ttl = Some(Duration::from_secs(1));
4324 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 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 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 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 #[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 #[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 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 #[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 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 const PRE_NEW_PROTOCOL_UPGRADE: Upgrade =
4584 Upgrade::new(EPOCH_REWARD_VERSION, NEW_PROTOCOL_VERSION);
4585
4586 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 #[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 #[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 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 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 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 tracing::info!("shutting down nodes");
4714 network.stop_consensus().await;
4715
4716 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 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 let state = network.server.decided_state().await.unwrap();
4739 tracing::info!(?decided_view, ?state, "consensus state");
4740
4741 drop(network);
4743
4744 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 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 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 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 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 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 assert_eq!(config.node_index, 1);
4830
4831 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 #[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 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 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 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 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 #[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 #[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 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 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 #[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 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 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 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 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 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 #[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 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 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 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 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 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 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 #[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 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 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 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 let mut events = network.peers[0].event_stream();
6347 wait_for_epochs(&mut events, EPOCH_HEIGHT, EPOCHS_BEFORE_RESTART).await;
6348
6349 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 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 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 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 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 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 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 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 #[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 #[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 #[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 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 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 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 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 let (storage, persistence_options) = node_storage::<D, NUM_NODES>().await;
6930
6931 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 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 tracing::info!("Shutting down peer 0");
6967 network.peers.remove(0);
6968
6969 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 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 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 let (storage, persistence_options) = node_storage::<D, NUM_NODES>().await;
7050
7051 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 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 tracing::info!("Shutting down peer 0");
7087 network.peers.remove(0);
7088
7089 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 if height > EPOCH_HEIGHT * 7 {
7101 break;
7102 }
7103 }
7104 }
7105
7106 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 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 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 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 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 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 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 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_epochs(&mut events, EPOCH_HEIGHT, 1).await;
7280
7281 node_0.shut_down().await;
7284
7285 wait_for_epochs(&mut events, EPOCH_HEIGHT, 4).await;
7287
7288 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 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 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 #[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 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 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 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 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 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 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 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 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 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 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
7558 enum DataPruned {
7559 BeforeRestart,
7562 WhileWaiting,
7568 }
7569
7570 #[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 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 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 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 wait_until_block_height(&query_client, "block-state/block-height", 5).await;
7664 query_node.shut_down().await;
7665 drop(query_node);
7666 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 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 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 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 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 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 #[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 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 let pruned = if stamp_pruned_height {
7795 D::prune_data_to(&db, stamp).await?;
7796 stamp
7797 } else {
7798 0
7799 };
7800 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 const PRUNED_START_EPOCH_HEIGHT: u64 = 10;
7845
7846 struct PrunedStartNetwork<D: TestableSqlDataSource> {
7850 network: TestNetwork<D::Options, 5>,
7851 storage: Vec<D::Storage>,
7852 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 async fn query_database(
7915 &self,
7916 expected: EmptyStateStart,
7917 ) -> anyhow::Result<(persistence::sql::Options, D::SqlStorage)> {
7918 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 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 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 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 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 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 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 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 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 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 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 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 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_for_epochs(&mut events, EPOCH_HEIGHT, 3).await;
8221
8222 let instance = node_0.node_state();
8223 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert!(v1_window["prev"].is_null(), "{v1_window}");
9559 assert!(v2_window.prev.is_none());
9560
9561 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 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 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 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 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 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 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 let leaf = network.server.decided_leaf().await;
10061 let catchup_height = leaf.height();
10062 let catchup_view = leaf.view_number().u64();
10063 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 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 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 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 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 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 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 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 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 assert!(
10527 v2_tables
10528 .tables
10529 .iter()
10530 .any(|table| table.table_name.ends_with("header")),
10531 "{v2_tables:?}"
10532 );
10533 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 {
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 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 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 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 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 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 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 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 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 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 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 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 "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 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 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 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 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 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 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 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 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 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 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 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 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 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 for namespace in 1..=4 {
11703 for _count in 0..namespace {
11704 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 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 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 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 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 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 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 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_decide_on_handle(&mut events, &txn).await;
11923 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 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 #[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 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 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_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 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 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 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 #[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 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 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 #[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_for_epochs(&mut events, EPOCH_HEIGHT, 3).await;
12282
12283 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_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 let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available");
12366
12367 let (storage, persistence) = node_storage::<D, NUM_NODES>().await;
12369
12370 let network_config = TestConfigBuilder::default()
12372 .epoch_height(EPOCH_HEIGHT)
12373 .build();
12374
12375 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 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 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 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 assert!(new_amounts[0] < tolerance);
12490
12491 assert!(new_amounts[1] + new_amounts[2] > tolerance);
12493
12494 Ok(())
12495 }
12496
12497 #[apply(sql_data_sources)]
12500 async fn test_integration_update_fast_finality_network_config<D: TestableSqlDataSource>(
12501 _d: PhantomData<D>,
12502 ) -> anyhow::Result<()> {
12503 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 #[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 let mut first_seen = None;
12593 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 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 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 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 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 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 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 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 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 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 let height = decided_leaf.height() / EPOCH_HEIGHT * EPOCH_HEIGHT;
12870
12871 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 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_until_block_height(&client, "node/block-height", avail_block).await;
12899
12900 let sample_start = Instant::now();
12908 let fee_account = 'fee_account: loop {
12909 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 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 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 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 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 assert_json_endpoint(
13113 &http,
13114 api_port,
13115 &format!("availability/block/{avail_block}/namespace/{avail_ns}"),
13116 )
13117 .await?;
13118
13119 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 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 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 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 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 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 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 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 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 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 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 assert_json_endpoint(&http, api_port, "availability/limits").await?;
13347
13348 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 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 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 assert_ws_endpoint_binary(api_port, &format!("availability/stream/leaves/{ws_start}"))
13422 .await?;
13423
13424 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 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 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 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 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 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 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 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 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 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 assert_endpoint_status(
13634 &http,
13635 api_port,
13636 "catchup/chain-config/CHAINCONFIG~AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
13637 400,
13638 )
13639 .await?;
13640 assert_endpoint_status(&http, api_port, "catchup/999999/leafchain", 404).await?;
13642 assert_endpoint_status(&http, api_port, "catchup/999999/cert2", 404).await?;
13644 assert_endpoint_status(&http, api_port, "catchup/1/reward-amounts/100/0", 404).await?;
13646
13647 assert_post_binary(
13653 &http,
13654 api_port,
13655 &format!("catchup/{height}/{decided_view}/accounts"),
13656 &vec![fee_account],
13657 )
13658 .await?;
13659 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 assert_endpoint_status(&http, api_port, "state-signature/block/999999", 404).await?;
13672
13673 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 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 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 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 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 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 assert_endpoint_status(&http, api_port, "availability/leaf/999999", 404).await?;
13796
13797 assert_endpoint_status(
13800 &http,
13801 api_port,
13802 &format!("availability/block/{avail_block}/{}", avail_block + 200),
13803 400,
13804 )
13805 .await?;
13806
13807 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 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 let mut events = network.peers[0].event_stream();
13881 wait_for_epochs(&mut events, EPOCH_HEIGHT, 3).await;
13882
13883 {
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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 .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 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 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 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 network.stop_consensus().await;
14429
14430 let heights =
14433 (0..=1)
14435 .chain(epoch_heights[0]-1..=epoch_heights[0] + 1)
14437 .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 #[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 #[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 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 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 #[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 const RESTART_EPOCH: u64 = 4;
14566 const RESTART_BOUNDARY: u64 = RESTART_EPOCH * EPOCH_HEIGHT;
14567 const RESTART_HEIGHT: u64 = RESTART_BOUNDARY - 3;
14568 const BLOCKS_AFTER_RESTART: u64 = 5;
14570
14571 let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
14572
14573 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 {
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 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 let saved_cfg = network.cfg.clone();
14630 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 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 let target_height = restart_height + BLOCKS_AFTER_RESTART;
14650 let stall_limit = 30; 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}