diff --git a/integration_tests/tests/indexer_ffi_helpers/mod.rs b/integration_tests/tests/indexer_ffi_helpers/mod.rs index c3c3caff..fe8f8989 100644 --- a/integration_tests/tests/indexer_ffi_helpers/mod.rs +++ b/integration_tests/tests/indexer_ffi_helpers/mod.rs @@ -57,9 +57,12 @@ pub fn setup_indexer_ffi( temp_indexer_dir.path().display() ); - let indexer_config = - integration_tests::config::indexer_config(bedrock_addr, temp_indexer_dir.path().to_owned()) - .context("Failed to create Indexer config")?; + let indexer_config = integration_tests::config::indexer_config( + bedrock_addr, + temp_indexer_dir.path().to_owned(), + integration_tests::config::bedrock_channel_id(), + ) + .context("Failed to create Indexer config")?; let config_json = serde_json::to_vec(&indexer_config)?; let config_path = temp_indexer_dir.path().join("indexer_config.json"); diff --git a/integration_tests/tests/two_zone.rs b/integration_tests/tests/two_zone.rs new file mode 100644 index 00000000..1b1ddefc --- /dev/null +++ b/integration_tests/tests/two_zone.rs @@ -0,0 +1,109 @@ +#![expect( + clippy::tests_outside_test_module, + clippy::arithmetic_side_effects, + reason = "We don't care about these in tests" +)] + +//! Two zones (sequencer + indexer each, on separate channels) sharing one +//! Bedrock node, each producing and finalizing blocks independently. + +use std::{net::SocketAddr, time::Duration}; + +use anyhow::{Context as _, Result}; +use indexer_service_rpc::RpcClient as _; +use integration_tests::{ + config::{self, SequencerPartialConfig}, + indexer_client::IndexerClient, + setup::{setup_bedrock_node, setup_indexer, setup_sequencer}, +}; +use sequencer_service_rpc::{RpcClient as _, SequencerClientBuilder}; +use tokio::test; + +const ZONE_LIVE_TIMEOUT: Duration = Duration::from_secs(360); + +// Genesis is block 1, so reaching 2 means a block was produced past it. +const MIN_BLOCK_ID: u64 = 2; + +#[test] +async fn two_zones_share_one_bedrock_and_both_advance() -> Result<()> { + // Declared first so it outlives both zones (drops run in reverse order). + let (_bedrock, bedrock_addr) = setup_bedrock_node() + .await + .context("Failed to set up shared Bedrock node")?; + + let partial = SequencerPartialConfig::default(); + let channel_a = config::bedrock_channel_id(); + let channel_b = config::bedrock_channel_id_b(); + + // Empty genesis is enough: the clock transaction drives block production. + let (seq_a, _seq_a_home) = setup_sequencer(partial, bedrock_addr, vec![], channel_a) + .await + .context("Failed to set up zone A sequencer")?; + let (idx_a, _idx_a_home) = setup_indexer(bedrock_addr, channel_a) + .await + .context("Failed to set up zone A indexer")?; + let (seq_b, _seq_b_home) = setup_sequencer(partial, bedrock_addr, vec![], channel_b) + .await + .context("Failed to set up zone B sequencer")?; + let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b) + .await + .context("Failed to set up zone B indexer")?; + + let (height_a, height_b) = tokio::try_join!( + wait_until_zone_live("A", seq_a.addr(), idx_a.addr()), + wait_until_zone_live("B", seq_b.addr(), idx_b.addr()), + )?; + + assert!( + height_a >= MIN_BLOCK_ID, + "Zone A indexer only reached block {height_a}, expected >= {MIN_BLOCK_ID}" + ); + assert!( + height_b >= MIN_BLOCK_ID, + "Zone B indexer only reached block {height_b}, expected >= {MIN_BLOCK_ID}" + ); + + Ok(()) +} + +/// Wait for the sequencer to produce past genesis and the indexer to finalize up +/// to it. Returns the indexer's finalized block id. +async fn wait_until_zone_live( + label: &str, + sequencer_addr: SocketAddr, + indexer_addr: SocketAddr, +) -> Result { + let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_addr) + .context("Failed to build sequencer URL")?; + let sequencer = SequencerClientBuilder::default() + .build(sequencer_url) + .context("Failed to build sequencer client")?; + + let indexer_url = config::addr_to_url(config::UrlProtocol::Ws, indexer_addr) + .context("Failed to build indexer URL")?; + let indexer = IndexerClient::new(&indexer_url) + .await + .context("Failed to build indexer client")?; + + let wait = async { + loop { + if sequencer.get_last_block_id().await? >= MIN_BLOCK_ID { + break; + } + tokio::time::sleep(Duration::from_secs(2)).await; + } + let target = sequencer.get_last_block_id().await?; + loop { + let finalized = indexer.get_last_finalized_block_id().await?.unwrap_or(0); + if finalized >= target { + log::info!("Zone {label} live: sequencer at {target}, indexer finalized {finalized}"); + return Ok::(finalized); + } + tokio::time::sleep(Duration::from_secs(2)).await; + } + }; + + tokio::time::timeout(ZONE_LIVE_TIMEOUT, wait) + .await + .with_context(|| format!("Zone {label} did not become live within {ZONE_LIVE_TIMEOUT:?}"))? +} diff --git a/test_fixtures/src/config.rs b/test_fixtures/src/config.rs index 73cd775b..fe61a348 100644 --- a/test_fixtures/src/config.rs +++ b/test_fixtures/src/config.rs @@ -67,6 +67,7 @@ pub fn sequencer_config( home: PathBuf, bedrock_addr: SocketAddr, genesis_transactions: Vec, + channel_id: ChannelId, ) -> Result { let SequencerPartialConfig { max_num_tx_in_block, @@ -85,7 +86,7 @@ pub fn sequencer_config( genesis: genesis_transactions, signing_key: [37; 32], bedrock_config: BedrockConfig { - channel_id: bedrock_channel_id(), + channel_id, node_url: addr_to_url(UrlProtocol::Http, bedrock_addr) .context("Failed to convert bedrock addr to URL")?, auth: None, @@ -163,7 +164,11 @@ pub fn wallet_config(sequencer_addr: SocketAddr) -> Result { }) } -pub fn indexer_config(bedrock_addr: SocketAddr, home: PathBuf) -> Result { +pub fn indexer_config( + bedrock_addr: SocketAddr, + home: PathBuf, + channel_id: ChannelId, +) -> Result { Ok(IndexerConfig { home, consensus_info_polling_interval: Duration::from_secs(1), @@ -172,7 +177,7 @@ pub fn indexer_config(bedrock_addr: SocketAddr, home: PathBuf) -> Result ChannelId { .unwrap_or_else(|_| unreachable!()); ChannelId::from(channel_id) } + +/// Second channel on the same Bedrock node, for two-zone tests. +/// Distinct from [`bedrock_channel_id`] so two zones settle independently on +/// one shared L1. +#[must_use] +pub fn bedrock_channel_id_b() -> ChannelId { + let channel_id: [u8; 32] = [0_u8, 2] + .repeat(16) + .try_into() + .unwrap_or_else(|_| unreachable!()); + ChannelId::from(channel_id) +} diff --git a/test_fixtures/src/lib.rs b/test_fixtures/src/lib.rs index 75394662..8ca97147 100644 --- a/test_fixtures/src/lib.rs +++ b/test_fixtures/src/lib.rs @@ -307,9 +307,10 @@ impl TestContextBuilder { .context("Failed to setup Bedrock node")?; let indexer_components = if enable_indexer { - let (indexer_handle, temp_indexer_dir) = setup_indexer(bedrock_addr) - .await - .context("Failed to setup Indexer")?; + let (indexer_handle, temp_indexer_dir) = + setup_indexer(bedrock_addr, config::bedrock_channel_id()) + .await + .context("Failed to setup Indexer")?; let indexer_url = config::addr_to_url(config::UrlProtocol::Ws, indexer_handle.addr()) .context("Failed to convert indexer addr to URL")?; let indexer_client = IndexerClient::new(&indexer_url) @@ -342,6 +343,7 @@ impl TestContextBuilder { sequencer_partial_config.unwrap_or_default(), bedrock_addr, genesis, + config::bedrock_channel_id(), ) .await .context("Failed to setup Sequencer")?; diff --git a/test_fixtures/src/setup.rs b/test_fixtures/src/setup.rs index 7eb0e1fd..85652e2f 100644 --- a/test_fixtures/src/setup.rs +++ b/test_fixtures/src/setup.rs @@ -1,7 +1,7 @@ use std::{net::SocketAddr, path::PathBuf}; use anyhow::{Context as _, Result, bail}; -use indexer_service::IndexerHandle; +use indexer_service::{ChannelId, IndexerHandle}; use lee::{AccountId, PrivateKey, PublicKey}; use log::{debug, warn}; use sequencer_service::{GenesisAction, SequencerHandle}; @@ -89,7 +89,10 @@ pub async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)> { Ok((compose, addr)) } -pub async fn setup_indexer(bedrock_addr: SocketAddr) -> Result<(IndexerHandle, TempDir)> { +pub async fn setup_indexer( + bedrock_addr: SocketAddr, + channel_id: ChannelId, +) -> Result<(IndexerHandle, TempDir)> { let temp_indexer_dir = tempfile::tempdir().context("Failed to create temp dir for indexer home")?; @@ -98,8 +101,9 @@ pub async fn setup_indexer(bedrock_addr: SocketAddr) -> Result<(IndexerHandle, T temp_indexer_dir.path().display() ); - let indexer_config = config::indexer_config(bedrock_addr, temp_indexer_dir.path().to_owned()) - .context("Failed to create Indexer config")?; + let indexer_config = + config::indexer_config(bedrock_addr, temp_indexer_dir.path().to_owned(), channel_id) + .context("Failed to create Indexer config")?; indexer_service::run_server(indexer_config, 0) .await @@ -111,6 +115,7 @@ pub async fn setup_sequencer( partial: config::SequencerPartialConfig, bedrock_addr: SocketAddr, genesis_transactions: Vec, + channel_id: ChannelId, ) -> Result<(SequencerHandle, TempDir)> { let temp_sequencer_dir = tempfile::tempdir().context("Failed to create temp dir for sequencer home")?; @@ -125,6 +130,7 @@ pub async fn setup_sequencer( temp_sequencer_dir.path().to_owned(), bedrock_addr, genesis_transactions, + channel_id, ) .context("Failed to create Sequencer config")?;