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