From ef886d8edff90e9d7db4c68acdcbe453f953087f Mon Sep 17 00:00:00 2001 From: Pacu Date: Sun, 26 Apr 2026 20:36:53 -0300 Subject: [PATCH 1/2] [#1057] Pipe zainod's binary version into LightdInfo PR description (wrapped at 80 cols): ## Problem `LightdInfo.version` reports `v0.1.0` even when running `zainod 0.2.0`. The field was sourced from `zaino-state`'s `CARGO_PKG_VERSION` (via `build.rs` -> `env!("VERSION")`). #939 decoupled library versions from `zainod`'s, and #1024 removed `workspace.package.version` entirely -- so the wire kept reporting the library version. Re-coupling isn't an option: the release ADR mandates per-crate independent versioning. The wire must follow the deployed binary. ## Fix Thread the binary version through at the config boundary: - `StateServiceConfig` / `FetchServiceConfig` gain `indexer_version: String`, defaulting to the library's own `CARGO_PKG_VERSION` for direct callers and tests. - `zainod`'s `TryFrom` impls overwrite it with `env!("CARGO_PKG_VERSION")`. - `get_build_info` takes the version as a parameter; both `spawn` sites pass `config.indexer_version`. Future `zainod` bumps now flow onto the wire automatically. Co-Authored-By: Claude Opus 4.7 --- packages/zaino-state/build.rs | 4 ---- packages/zaino-state/src/backends/fetch.rs | 2 +- packages/zaino-state/src/backends/state.rs | 2 +- packages/zaino-state/src/config.rs | 18 ++++++++++++++++++ packages/zaino-state/src/utils.rs | 22 ++++++++++++++++++++-- packages/zainod/src/config.rs | 22 ++++++++++++++++++++++ 6 files changed, 62 insertions(+), 8 deletions(-) diff --git a/packages/zaino-state/build.rs b/packages/zaino-state/build.rs index d194481d3..40efa6911 100644 --- a/packages/zaino-state/build.rs +++ b/packages/zaino-state/build.rs @@ -41,9 +41,5 @@ fn main() -> io::Result<()> { let build_user = whoami::username(); println!("cargo:rustc-env=BUILD_USER={build_user}"); - // Set the version from Cargo.toml - let version = env::var("CARGO_PKG_VERSION").expect("Failed to get version from Cargo.toml"); - println!("cargo:rustc-env=VERSION={version}"); - Ok(()) } diff --git a/packages/zaino-state/src/backends/fetch.rs b/packages/zaino-state/src/backends/fetch.rs index 1efcc71fb..2a8e18458 100644 --- a/packages/zaino-state/src/backends/fetch.rs +++ b/packages/zaino-state/src/backends/fetch.rs @@ -136,7 +136,7 @@ impl ZcashService for FetchService { let zebra_build_data = fetcher.get_info().await?; let data = ServiceMetadata::new( - get_build_info(), + get_build_info(config.indexer_version.clone()), config.network.to_zebra_network(), zebra_build_data.build, zebra_build_data.subversion, diff --git a/packages/zaino-state/src/backends/state.rs b/packages/zaino-state/src/backends/state.rs index 2fedecc81..d4614add1 100644 --- a/packages/zaino-state/src/backends/state.rs +++ b/packages/zaino-state/src/backends/state.rs @@ -198,7 +198,7 @@ impl ZcashService for StateService { let zebra_build_data = rpc_client.get_info().await?; let data = ServiceMetadata::new( - get_build_info(), + get_build_info(config.indexer_version.clone()), config.network.to_zebra_network(), zebra_build_data.build, zebra_build_data.subversion, diff --git a/packages/zaino-state/src/config.rs b/packages/zaino-state/src/config.rs index 15c1ca304..ce1ca492f 100644 --- a/packages/zaino-state/src/config.rs +++ b/packages/zaino-state/src/config.rs @@ -95,6 +95,14 @@ pub struct StateServiceConfig { pub network: Network, /// Zcash donation UA address pub donation_address: Option, + /// Version of the indexer binary embedding this service. + /// + /// Reported on the wire via `LightdInfo.version`. Defaults to this + /// crate's `CARGO_PKG_VERSION` when constructed via [`Self::new`]; + /// the embedding binary should overwrite it with its own + /// `CARGO_PKG_VERSION` so the wire reflects the deployed indexer + /// rather than the library crate. + pub indexer_version: String, } #[allow(deprecated)] @@ -131,6 +139,7 @@ impl StateServiceConfig { storage, network, donation_address, + indexer_version: env!("CARGO_PKG_VERSION").to_string(), } } } @@ -155,6 +164,14 @@ pub struct FetchServiceConfig { pub network: Network, /// Zcash donation UA address pub donation_address: Option, + /// Version of the indexer binary embedding this service. + /// + /// Reported on the wire via `LightdInfo.version`. Defaults to this + /// crate's `CARGO_PKG_VERSION` when constructed via [`Self::new`]; + /// the embedding binary should overwrite it with its own + /// `CARGO_PKG_VERSION` so the wire reflects the deployed indexer + /// rather than the library crate. + pub indexer_version: String, } #[allow(deprecated)] @@ -180,6 +197,7 @@ impl FetchServiceConfig { storage, network, donation_address, + indexer_version: env!("CARGO_PKG_VERSION").to_string(), } } } diff --git a/packages/zaino-state/src/utils.rs b/packages/zaino-state/src/utils.rs index 70c935f48..46f11fada 100644 --- a/packages/zaino-state/src/utils.rs +++ b/packages/zaino-state/src/utils.rs @@ -53,13 +53,17 @@ impl fmt::Display for BuildInfo { } /// Returns build info for Zingo-Indexer. -pub(crate) fn get_build_info() -> BuildInfo { +/// +/// `version` is the version of the deployed indexer binary (e.g. `zainod`), +/// supplied by the caller. Library crates do not know which binary embeds +/// them, so each binary passes its own `CARGO_PKG_VERSION`. +pub(crate) fn get_build_info(version: String) -> BuildInfo { BuildInfo { commit_hash: env!("GIT_COMMIT").to_string(), branch: env!("BRANCH").to_string(), build_date: env!("BUILD_DATE").to_string(), build_user: env!("BUILD_USER").to_string(), - version: env!("VERSION").to_string(), + version, } } @@ -113,3 +117,17 @@ impl fmt::Display for ServiceMetadata { writeln!(f, "Zebra Subversion: {}", self.zebra_subversion) } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Regression test for issue #1057: the version flowed onto the wire via + /// `LightdInfo.version` must come from the caller-supplied string (the + /// embedding binary's `CARGO_PKG_VERSION`), not from this library crate. + #[test] + fn get_build_info_uses_caller_supplied_version() { + let build_info = get_build_info("9.9.9-test".to_string()); + assert_eq!(build_info.version(), "9.9.9-test"); + } +} diff --git a/packages/zainod/src/config.rs b/packages/zainod/src/config.rs index 4ca8018c8..4864798be 100644 --- a/packages/zainod/src/config.rs +++ b/packages/zainod/src/config.rs @@ -378,6 +378,7 @@ impl TryFrom for StateServiceConfig { storage: cfg.storage, network: cfg.network, donation_address: cfg.donation_address, + indexer_version: env!("CARGO_PKG_VERSION").to_string(), }) } } @@ -402,6 +403,7 @@ impl TryFrom for FetchServiceConfig { storage: cfg.storage, network: cfg.network, donation_address: cfg.donation_address, + indexer_version: env!("CARGO_PKG_VERSION").to_string(), }) } } @@ -1040,4 +1042,24 @@ listen_address = "127.0.0.1:8137" let path = create_test_config_file(&dir, content, "invalid_donation.toml"); assert!(load_config(&path).is_err()); } + + /// `LightdInfo.version` (issue #1057) is sourced from + /// `*ServiceConfig.indexer_version`. This must be set to `zainod`'s + /// `CARGO_PKG_VERSION` at the boundary so the wire reflects the + /// deployed binary, not zaino-state's library version. + #[test] + #[allow(deprecated)] + fn indexer_version_is_zainod_pkg_version() { + let _guard = EnvGuard::new(); + + let cfg = ZainodConfig::default(); + + let state_cfg = StateServiceConfig::try_from(cfg.clone()) + .expect("StateServiceConfig conversion should succeed for default ZainodConfig"); + assert_eq!(state_cfg.indexer_version, env!("CARGO_PKG_VERSION")); + + let fetch_cfg = FetchServiceConfig::try_from(cfg) + .expect("FetchServiceConfig conversion should succeed for default ZainodConfig"); + assert_eq!(fetch_cfg.indexer_version, env!("CARGO_PKG_VERSION")); + } } From bc6f5d2ce363264cae30a1c6fbf989470f3918d7 Mon Sep 17 00:00:00 2001 From: zancas Date: Mon, 27 Apr 2026 16:47:48 -0700 Subject: [PATCH 2/2] refactor(zaino-state): extract CommonBackendConfig shared payload StateServiceConfig and FetchServiceConfig duplicated 9 of their fields (rpc address, cookie path, user/password, service, storage, network, donation address, indexer_version) and the duplication had already bred copy-paste drift, including the indexer_version doc block this branch exists to address. Introduces CommonBackendConfig holding the shared bits; both Service configs now compose it via a `common` field plus their backend-specific extras. Knock-on cleanups that fall out: - The two `From<*ServiceConfig> for BlockCacheConfig` impls collapse into one `From` plus trivial delegators. - zainod's `TryFrom` impls share a new `build_common` helper, so the "xxxxxx" missing-credentials sentinel and `env!("CARGO_PKG_VERSION")` are written once instead of twice. - The "authentification" doc-comment typo is fixed in passing on the new shared struct. Both `new()` constructor signatures are preserved, so the integration tests that call them positionally keep compiling without churn. Adds `state_and_fetch_common_payloads_agree` next to `indexer_version_is_zainod_pkg_version` to lock in the property the refactor establishes: both `TryFrom` paths must produce the same `CommonBackendConfig`. Pretty-Debug equality is used so the assertion auto-covers any field added to the struct later. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/zaino-state/src/backends/fetch.rs | 55 ++++----- packages/zaino-state/src/backends/state.rs | 76 +++++++------ packages/zaino-state/src/config.rs | 124 ++++++++++----------- packages/zaino-state/src/lib.rs | 4 +- packages/zainod/src/config.rs | 115 +++++++++++-------- 5 files changed, 196 insertions(+), 178 deletions(-) diff --git a/packages/zaino-state/src/backends/fetch.rs b/packages/zaino-state/src/backends/fetch.rs index 2a8e18458..86f942c16 100644 --- a/packages/zaino-state/src/backends/fetch.rs +++ b/packages/zaino-state/src/backends/fetch.rs @@ -118,26 +118,26 @@ impl ZcashService for FetchService { type Config = FetchServiceConfig; /// Initializes a new FetchService instance and starts sync process. - #[instrument(name = "FetchService::spawn", skip(config), fields(network = %config.network))] + #[instrument(name = "FetchService::spawn", skip(config), fields(network = %config.common.network))] async fn spawn(config: FetchServiceConfig) -> Result { info!( - rpc_address = %config.validator_rpc_address, - network = %config.network, + rpc_address = %config.common.validator_rpc_address, + network = %config.common.network, "Launching Fetch Service" ); let fetcher = JsonRpSeeConnector::new_from_config_parts( - &config.validator_rpc_address, - config.validator_rpc_user.clone(), - config.validator_rpc_password.clone(), - config.validator_cookie_path.clone(), + &config.common.validator_rpc_address, + config.common.validator_rpc_user.clone(), + config.common.validator_rpc_password.clone(), + config.common.validator_cookie_path.clone(), ) .await?; let zebra_build_data = fetcher.get_info().await?; let data = ServiceMetadata::new( - get_build_info(config.indexer_version.clone()), - config.network.to_zebra_network(), + get_build_info(config.common.indexer_version.clone()), + config.common.network.to_zebra_network(), zebra_build_data.build, zebra_build_data.subversion, ); @@ -235,7 +235,7 @@ impl FetchServiceSubscriber { /// Returns the network type running. #[allow(deprecated)] pub fn network(&self) -> zaino_common::Network { - self.config.network + self.config.common.network } } @@ -1011,8 +1011,8 @@ impl LightWalletIndexer for FetchServiceSubscriber { let end = validated_request.end() as u32; let fetch_service_clone = self.clone(); - let service_timeout = self.config.service.timeout; - let (channel_tx, channel_rx) = mpsc::channel(self.config.service.channel_size as usize); + let service_timeout = self.config.common.service.timeout; + let (channel_tx, channel_rx) = mpsc::channel(self.config.common.service.channel_size as usize); let snapshot = fetch_service_clone .indexer .snapshot_nonfinalized_state() @@ -1143,8 +1143,8 @@ impl LightWalletIndexer for FetchServiceSubscriber { let end = validated_request.end() as u32; let fetch_service_clone = self.clone(); - let service_timeout = self.config.service.timeout; - let (channel_tx, channel_rx) = mpsc::channel(self.config.service.channel_size as usize); + let service_timeout = self.config.common.service.timeout; + let (channel_tx, channel_rx) = mpsc::channel(self.config.common.service.channel_size as usize); let snapshot = fetch_service_clone .indexer .snapshot_nonfinalized_state() @@ -1331,8 +1331,8 @@ impl LightWalletIndexer for FetchServiceSubscriber { let chain_height = self.chain_height().await?; let txids = self.get_taddress_txids_helper(request).await?; let fetch_service_clone = self.clone(); - let service_timeout = self.config.service.timeout; - let (transmitter, receiver) = mpsc::channel(self.config.service.channel_size as usize); + let service_timeout = self.config.common.service.timeout; + let (transmitter, receiver) = mpsc::channel(self.config.common.service.channel_size as usize); tokio::spawn(async move { let timeout = timeout( time::Duration::from_secs((service_timeout * 4) as u64), @@ -1403,9 +1403,9 @@ impl LightWalletIndexer for FetchServiceSubscriber { mut request: AddressStream, ) -> Result { let fetch_service_clone = self.clone(); - let service_timeout = self.config.service.timeout; + let service_timeout = self.config.common.service.timeout; let (channel_tx, mut channel_rx) = - mpsc::channel::(self.config.service.channel_size as usize); + mpsc::channel::(self.config.common.service.channel_size as usize); let fetcher_task_handle = tokio::spawn(async move { let fetcher_timeout = timeout( time::Duration::from_secs((service_timeout * 4) as u64), @@ -1537,8 +1537,8 @@ impl LightWalletIndexer for FetchServiceSubscriber { } let mempool = self.indexer.clone(); - let service_timeout = self.config.service.timeout; - let (channel_tx, channel_rx) = mpsc::channel(self.config.service.channel_size as usize); + let service_timeout = self.config.common.service.timeout; + let (channel_tx, channel_rx) = mpsc::channel(self.config.common.service.channel_size as usize); tokio::spawn(async move { let timeout = timeout( @@ -1637,8 +1637,8 @@ impl LightWalletIndexer for FetchServiceSubscriber { #[allow(deprecated)] async fn get_mempool_stream(&self) -> Result { let indexer = self.indexer.clone(); - let service_timeout = self.config.service.timeout; - let (channel_tx, channel_rx) = mpsc::channel(self.config.service.channel_size as usize); + let service_timeout = self.config.common.service.timeout; + let (channel_tx, channel_rx) = mpsc::channel(self.config.common.service.channel_size as usize); let snapshot = indexer.snapshot_nonfinalized_state().await?; tokio::spawn(async move { let timeout = timeout( @@ -1733,7 +1733,7 @@ impl LightWalletIndexer for FetchServiceSubscriber { .await? .into_parts(); Ok(TreeState { - network: self.config.network.to_zebra_network().bip70_network_name(), + network: self.config.common.network.to_zebra_network().bip70_network_name(), height: height.0 as u64, hash: hash.to_string(), time, @@ -1755,8 +1755,8 @@ impl LightWalletIndexer for FetchServiceSubscriber { #[allow(deprecated)] fn timeout_channel_size(&self) -> (u32, u32) { ( - self.config.service.timeout, - self.config.service.channel_size, + self.config.common.service.timeout, + self.config.common.service.channel_size, ) } @@ -1825,8 +1825,8 @@ impl LightWalletIndexer for FetchServiceSubscriber { ) -> Result { let taddrs = GetAddressBalanceRequest::new(request.addresses); let utxos = self.z_get_address_utxos(taddrs).await?; - let service_timeout = self.config.service.timeout; - let (channel_tx, channel_rx) = mpsc::channel(self.config.service.channel_size as usize); + let service_timeout = self.config.common.service.timeout; + let (channel_tx, channel_rx) = mpsc::channel(self.config.common.service.channel_size as usize); tokio::spawn(async move { let timeout = timeout( time::Duration::from_secs((service_timeout * 4) as u64), @@ -1944,6 +1944,7 @@ impl LightWalletIndexer for FetchServiceSubscriber { zcashd_subversion: self.data.zebra_subversion(), donation_address: self .config + .common .donation_address .as_ref() .map(DonationAddress::encode) diff --git a/packages/zaino-state/src/backends/state.rs b/packages/zaino-state/src/backends/state.rs index d4614add1..7e6e38721 100644 --- a/packages/zaino-state/src/backends/state.rs +++ b/packages/zaino-state/src/backends/state.rs @@ -179,27 +179,27 @@ impl ZcashService for StateService { type Config = StateServiceConfig; /// Initializes a new StateService instance and starts sync process. - #[instrument(name = "StateService::spawn", skip(config), fields(network = %config.network))] + #[instrument(name = "StateService::spawn", skip(config), fields(network = %config.common.network))] async fn spawn(config: StateServiceConfig) -> Result { info!( - rpc_address = %config.validator_rpc_address, - network = %config.network, + rpc_address = %config.common.validator_rpc_address, + network = %config.common.network, "Spawning State Service" ); let rpc_client = JsonRpSeeConnector::new_from_config_parts( - &config.validator_rpc_address, - config.validator_rpc_user.clone(), - config.validator_rpc_password.clone(), - config.validator_cookie_path.clone(), + &config.common.validator_rpc_address, + config.common.validator_rpc_user.clone(), + config.common.validator_rpc_password.clone(), + config.common.validator_cookie_path.clone(), ) .await?; let zebra_build_data = rpc_client.get_info().await?; let data = ServiceMetadata::new( - get_build_info(config.indexer_version.clone()), - config.network.to_zebra_network(), + get_build_info(config.common.indexer_version.clone()), + config.common.network.to_zebra_network(), zebra_build_data.build, zebra_build_data.subversion, ); @@ -212,7 +212,7 @@ impl ZcashService for StateService { let (mut read_state_service, _latest_chain_tip, chain_tip_change, sync_task_handle) = init_read_state_with_syncer( config.validator_state_config.clone(), - &config.network.to_zebra_network(), + &config.common.network.to_zebra_network(), config.validator_grpc_address, ) .await??; @@ -251,7 +251,7 @@ impl ZcashService for StateService { let mempool_source = ValidatorConnector::State(crate::chain_index::source::State { read_state_service: read_state_service.clone(), mempool_fetcher: rpc_client.clone(), - network: config.network, + network: config.common.network, }); let mempool = Mempool::spawn(mempool_source, None).await?; @@ -260,7 +260,7 @@ impl ZcashService for StateService { ValidatorConnector::State(State { read_state_service: read_state_service.clone(), mempool_fetcher: rpc_client.clone(), - network: config.network, + network: config.common.network, }), config.clone().into(), ) @@ -558,8 +558,8 @@ impl StateServiceSubscriber { let end = validated_request.end() as u32; let state_service_clone = self.clone(); - let service_timeout = self.config.service.timeout; - let (channel_tx, channel_rx) = mpsc::channel(self.config.service.channel_size as usize); + let service_timeout = self.config.common.service.timeout; + let (channel_tx, channel_rx) = mpsc::channel(self.config.common.service.channel_size as usize); let snapshot = state_service_clone .indexer .snapshot_nonfinalized_state() @@ -950,7 +950,7 @@ impl StateServiceSubscriber { /// Returns the network type running. #[allow(deprecated)] pub fn network(&self) -> zaino_common::Network { - self.config.network + self.config.common.network } /// Returns the median time of the last 11 blocks. @@ -1114,7 +1114,7 @@ impl ZcashIndexer for StateServiceSubscriber { async fn get_difficulty(&self) -> Result { chain_tip_difficulty( - self.config.network.to_zebra_network(), + self.config.common.network.to_zebra_network(), self.read_state_service.clone(), false, ) @@ -1172,7 +1172,7 @@ impl ZcashIndexer for StateServiceSubscriber { let now = Utc::now(); let zebra_estimated_height = - NetworkChainTipHeightEstimator::new(header.time, height, &self.config.network.into()) + NetworkChainTipHeightEstimator::new(header.time, height, &self.config.common.network.into()) .estimate_height_at(now); let estimated_height = if header.time > now || zebra_estimated_height < height { height @@ -1182,6 +1182,7 @@ impl ZcashIndexer for StateServiceSubscriber { let upgrades = IndexMap::from_iter( self.config + .common .network .to_zebra_network() .full_activation_list() @@ -1217,14 +1218,14 @@ impl ZcashIndexer for StateServiceSubscriber { (height + 1).expect("valid chain tips are a lot less than Height::MAX"); let consensus = TipConsensusBranch::from_parts( ConsensusBranchIdHex::new( - NetworkUpgrade::current(&self.config.network.into(), height) + NetworkUpgrade::current(&self.config.common.network.into(), height) .branch_id() .unwrap_or(ConsensusBranchId::RPC_MISSING_ID) .into(), ) .inner(), ConsensusBranchIdHex::new( - NetworkUpgrade::current(&self.config.network.into(), next_block_height) + NetworkUpgrade::current(&self.config.common.network.into(), next_block_height) .branch_id() .unwrap_or(ConsensusBranchId::RPC_MISSING_ID) .into(), @@ -1234,7 +1235,7 @@ impl ZcashIndexer for StateServiceSubscriber { // TODO: Remove unwrap() let difficulty = chain_tip_difficulty( - self.config.network.to_zebra_network(), + self.config.common.network.to_zebra_network(), self.read_state_service.clone(), false, ) @@ -1244,7 +1245,7 @@ impl ZcashIndexer for StateServiceSubscriber { let verification_progress = f64::from(height.0) / f64::from(zebra_estimated_height.0); Ok(GetBlockchainInfoResponse::new( - self.config.network.to_zebra_network().bip70_network_name(), + self.config.common.network.to_zebra_network().bip70_network_name(), height, hash, estimated_height, @@ -1612,7 +1613,7 @@ impl ZcashIndexer for StateServiceSubscriber { }; let address = match address.convert_if_network::
( - match self.config.network.to_zebra_network().kind() { + match self.config.common.network.to_zebra_network().kind() { NetworkKind::Mainnet => NetworkType::Main, NetworkKind::Testnet => NetworkType::Test, NetworkKind::Regtest => NetworkType::Regtest, @@ -1649,7 +1650,7 @@ impl ZcashIndexer for StateServiceSubscriber { }; let converted_address = match parsed_address.convert_if_network::
( - match self.config.network.to_zebra_network().kind() { + match self.config.common.network.to_zebra_network().kind() { NetworkKind::Mainnet => NetworkType::Main, NetworkKind::Testnet => NetworkType::Test, NetworkKind::Regtest => NetworkType::Regtest, @@ -1807,7 +1808,7 @@ impl ZcashIndexer for StateServiceSubscriber { parsed_tx.into(), None, // best_chain_height Some(0), // confirmations - &self.config.network.into(), // network + &self.config.common.network.into(), // network None, // block_time None, // block_hash Some(false), // in_best_chain @@ -1850,7 +1851,7 @@ impl ZcashIndexer for StateServiceSubscriber { tx.tx.clone(), best_chain_height, Some(tx.confirmations), - &self.config.network.into(), + &self.config.common.network.into(), Some(tx.block_time), Some(zebra_chain::block::Hash::from_bytes(compact_block.hash)), Some(best_chain_height.is_some()), @@ -2189,8 +2190,8 @@ impl LightWalletIndexer for StateServiceSubscriber { ) -> Result { let txids = self.get_taddress_txids_helper(request).await?; let chain_height = self.chain_height().await?; - let (transmitter, receiver) = mpsc::channel(self.config.service.channel_size as usize); - let service_timeout = self.config.service.timeout; + let (transmitter, receiver) = mpsc::channel(self.config.common.service.channel_size as usize); + let service_timeout = self.config.common.service.timeout; let service_clone = self.clone(); tokio::spawn(async move { let timeout = timeout( @@ -2263,9 +2264,9 @@ impl LightWalletIndexer for StateServiceSubscriber { mut request: AddressStream, ) -> Result { let fetch_service_clone = self.clone(); - let service_timeout = self.config.service.timeout; + let service_timeout = self.config.common.service.timeout; let (channel_tx, mut channel_rx) = - mpsc::channel::(self.config.service.channel_size as usize); + mpsc::channel::(self.config.common.service.channel_size as usize); let fetcher_task_handle = tokio::spawn(async move { let fetcher_timeout = timeout( time::Duration::from_secs((service_timeout * 4) as u64), @@ -2405,8 +2406,8 @@ impl LightWalletIndexer for StateServiceSubscriber { }; let mempool = self.mempool.clone(); - let service_timeout = self.config.service.timeout; - let (channel_tx, channel_rx) = mpsc::channel(self.config.service.channel_size as usize); + let service_timeout = self.config.common.service.timeout; + let (channel_tx, channel_rx) = mpsc::channel(self.config.common.service.channel_size as usize); tokio::spawn(async move { let timeout = timeout( time::Duration::from_secs((service_timeout * 4) as u64), @@ -2495,8 +2496,8 @@ impl LightWalletIndexer for StateServiceSubscriber { /// there are mempool transactions. It will close the returned stream when a new block is mined. async fn get_mempool_stream(&self) -> Result { let mut mempool = self.mempool.clone(); - let service_timeout = self.config.service.timeout; - let (channel_tx, channel_rx) = mpsc::channel(self.config.service.channel_size as usize); + let service_timeout = self.config.common.service.timeout; + let (channel_tx, channel_rx) = mpsc::channel(self.config.common.service.channel_size as usize); let snapshot = self.indexer.snapshot_nonfinalized_state().await?; let Some(non_finalized_snapshot) = snapshot.get_nfs_snapshot() else { // TODO: This probably shouldn't be an error. @@ -2590,7 +2591,7 @@ impl LightWalletIndexer for StateServiceSubscriber { .await? .into_parts(); Ok(TreeState { - network: self.config.network.to_zebra_network().bip70_network_name(), + network: self.config.common.network.to_zebra_network().bip70_network_name(), height: height.0 as u64, hash: hash.to_string(), time, @@ -2611,8 +2612,8 @@ impl LightWalletIndexer for StateServiceSubscriber { fn timeout_channel_size(&self) -> (u32, u32) { ( - self.config.service.timeout, - self.config.service.channel_size, + self.config.common.service.timeout, + self.config.common.service.channel_size, ) } @@ -2657,7 +2658,7 @@ impl LightWalletIndexer for StateServiceSubscriber { .and_then(|service| service.call(ReadRequest::UtxosByAddresses(address_set))) .await?; let utxos = expected_read_response!(address_utxos_response, AddressUtxos); - let (channel_tx, channel_rx) = mpsc::channel(self.config.service.channel_size as usize); + let (channel_tx, channel_rx) = mpsc::channel(self.config.common.service.channel_size as usize); tokio::spawn(async move { for utxo in utxos .utxos() @@ -2739,6 +2740,7 @@ impl LightWalletIndexer for StateServiceSubscriber { zcashd_subversion: self.data.zebra_subversion(), donation_address: self .config + .common .donation_address .as_ref() .map(DonationAddress::encode) diff --git a/packages/zaino-state/src/config.rs b/packages/zaino-state/src/config.rs index ce1ca492f..f057aba3e 100644 --- a/packages/zaino-state/src/config.rs +++ b/packages/zaino-state/src/config.rs @@ -69,19 +69,15 @@ pub enum BackendConfig { Fetch(FetchServiceConfig), } -/// Holds config data for [crate::StateService]. +/// Configuration shared by every backend variant. +/// +/// Carries the validator-RPC connection bits plus the runtime indexer +/// settings that are independent of how blockchain data is fetched. #[derive(Debug, Clone)] -// #[deprecated] -pub struct StateServiceConfig { - /// Zebra [`zebra_state::ReadStateService`] config data - pub validator_state_config: zebra_state::Config, +pub struct CommonBackendConfig { /// Validator JsonRPC address (supports hostname:port or ip:port format). pub validator_rpc_address: String, - /// Validator gRPC address (requires ip:port format for Zebra state sync). - pub validator_grpc_address: std::net::SocketAddr, - /// Validator cookie auth. - pub validator_cookie_auth: bool, - /// Enable validator rpc cookie authentification with Some: Path to the validator cookie file. + /// Enable validator rpc cookie authentication with Some: path to the validator cookie file. pub validator_cookie_path: Option, /// Validator JsonRPC user. pub validator_rpc_user: String, @@ -98,18 +94,31 @@ pub struct StateServiceConfig { /// Version of the indexer binary embedding this service. /// /// Reported on the wire via `LightdInfo.version`. Defaults to this - /// crate's `CARGO_PKG_VERSION` when constructed via [`Self::new`]; - /// the embedding binary should overwrite it with its own - /// `CARGO_PKG_VERSION` so the wire reflects the deployed indexer - /// rather than the library crate. + /// crate's `CARGO_PKG_VERSION` when constructed via the parent + /// service's `new`; the embedding binary should overwrite it with + /// its own `CARGO_PKG_VERSION` so the wire reflects the deployed + /// indexer rather than the library crate. pub indexer_version: String, } +/// Holds config data for [crate::StateService]. +#[derive(Debug, Clone)] +// #[deprecated] +pub struct StateServiceConfig { + /// Settings shared with [`FetchServiceConfig`]. + pub common: CommonBackendConfig, + /// Zebra [`zebra_state::ReadStateService`] config data + pub validator_state_config: zebra_state::Config, + /// Validator gRPC address (requires ip:port format for Zebra state sync). + pub validator_grpc_address: std::net::SocketAddr, + /// Validator cookie auth. + pub validator_cookie_auth: bool, +} + #[allow(deprecated)] impl StateServiceConfig { /// Returns a new instance of [`StateServiceConfig`]. #[allow(clippy::too_many_arguments)] - // TODO: replace with struct-literal init only? pub fn new( validator_state_config: zebra_state::Config, validator_rpc_address: String, @@ -128,18 +137,20 @@ impl StateServiceConfig { network.to_zebra_network().full_activation_list() ); StateServiceConfig { + common: CommonBackendConfig { + validator_rpc_address, + validator_cookie_path, + validator_rpc_user: validator_rpc_user.unwrap_or("xxxxxx".to_string()), + validator_rpc_password: validator_rpc_password.unwrap_or("xxxxxx".to_string()), + service, + storage, + network, + donation_address, + indexer_version: env!("CARGO_PKG_VERSION").to_string(), + }, validator_state_config, - validator_rpc_address, validator_grpc_address, validator_cookie_auth, - validator_cookie_path, - validator_rpc_user: validator_rpc_user.unwrap_or("xxxxxx".to_string()), - validator_rpc_password: validator_rpc_password.unwrap_or("xxxxxx".to_string()), - service, - storage, - network, - donation_address, - indexer_version: env!("CARGO_PKG_VERSION").to_string(), } } } @@ -148,30 +159,8 @@ impl StateServiceConfig { #[derive(Debug, Clone)] #[deprecated] pub struct FetchServiceConfig { - /// Validator JsonRPC address (supports hostname:port or ip:port format). - pub validator_rpc_address: String, - /// Enable validator rpc cookie authentification with Some: path to the validator cookie file. - pub validator_cookie_path: Option, - /// Validator JsonRPC user. - pub validator_rpc_user: String, - /// Validator JsonRPC password. - pub validator_rpc_password: String, - /// Service-level configuration (timeout, channel size) - pub service: ServiceConfig, - /// Storage configuration (cache and database) - pub storage: StorageConfig, - /// Network type. - pub network: Network, - /// Zcash donation UA address - pub donation_address: Option, - /// Version of the indexer binary embedding this service. - /// - /// Reported on the wire via `LightdInfo.version`. Defaults to this - /// crate's `CARGO_PKG_VERSION` when constructed via [`Self::new`]; - /// the embedding binary should overwrite it with its own - /// `CARGO_PKG_VERSION` so the wire reflects the deployed indexer - /// rather than the library crate. - pub indexer_version: String, + /// Settings shared with [`StateServiceConfig`]. + pub common: CommonBackendConfig, } #[allow(deprecated)] @@ -189,15 +178,17 @@ impl FetchServiceConfig { donation_address: Option, ) -> Self { FetchServiceConfig { - validator_rpc_address, - validator_cookie_path, - validator_rpc_user: validator_rpc_user.unwrap_or("xxxxxx".to_string()), - validator_rpc_password: validator_rpc_password.unwrap_or("xxxxxx".to_string()), - service, - storage, - network, - donation_address, - indexer_version: env!("CARGO_PKG_VERSION").to_string(), + common: CommonBackendConfig { + validator_rpc_address, + validator_cookie_path, + validator_rpc_user: validator_rpc_user.unwrap_or("xxxxxx".to_string()), + validator_rpc_password: validator_rpc_password.unwrap_or("xxxxxx".to_string()), + service, + storage, + network, + donation_address, + indexer_version: env!("CARGO_PKG_VERSION").to_string(), + }, } } } @@ -226,9 +217,8 @@ impl BlockCacheConfig { } } -#[allow(deprecated)] -impl From for BlockCacheConfig { - fn from(value: StateServiceConfig) -> Self { +impl From for BlockCacheConfig { + fn from(value: CommonBackendConfig) -> Self { Self { storage: value.storage, // TODO: update zaino configs to include db version. @@ -238,15 +228,17 @@ impl From for BlockCacheConfig { } } +#[allow(deprecated)] +impl From for BlockCacheConfig { + fn from(value: StateServiceConfig) -> Self { + value.common.into() + } +} + #[allow(deprecated)] impl From for BlockCacheConfig { fn from(value: FetchServiceConfig) -> Self { - Self { - storage: value.storage, - // TODO: update zaino configs to include db version. - db_version: 1, - network: value.network, - } + value.common.into() } } diff --git a/packages/zaino-state/src/lib.rs b/packages/zaino-state/src/lib.rs index 8fd67c9b4..e63d9bd7b 100644 --- a/packages/zaino-state/src/lib.rs +++ b/packages/zaino-state/src/lib.rs @@ -64,8 +64,8 @@ pub(crate) mod config; #[allow(deprecated)] pub use config::{ - BackendConfig, BackendType, BlockCacheConfig, DonationAddress, FetchServiceConfig, - StateServiceConfig, + BackendConfig, BackendType, BlockCacheConfig, CommonBackendConfig, DonationAddress, + FetchServiceConfig, StateServiceConfig, }; pub(crate) mod error; diff --git a/packages/zainod/src/config.rs b/packages/zainod/src/config.rs index 4864798be..51703aa51 100644 --- a/packages/zainod/src/config.rs +++ b/packages/zainod/src/config.rs @@ -16,7 +16,9 @@ use zaino_common::{ }; use zaino_serve::server::config::{GrpcServerConfig, JsonRpcServerConfig}; #[allow(deprecated)] -use zaino_state::{BackendType, DonationAddress, FetchServiceConfig, StateServiceConfig}; +use zaino_state::{ + BackendType, CommonBackendConfig, DonationAddress, FetchServiceConfig, StateServiceConfig, +}; /// Header for generated configuration files. pub const GENERATED_CONFIG_HEADER: &str = r#"# Zaino Configuration @@ -349,36 +351,22 @@ impl TryFrom for StateServiceConfig { )) })?; + let validator_state_config = zebra_state::Config { + cache_dir: cfg.zebra_db_path.clone(), + ephemeral: false, + delete_old_database: true, + debug_stop_at_height: None, + debug_validity_check_interval: None, + should_backup_non_finalized_state: true, + debug_skip_non_finalized_state_backup_task: false, + }; + let validator_cookie_auth = cfg.validator_settings.validator_cookie_path.is_some(); + Ok(StateServiceConfig { - validator_state_config: zebra_state::Config { - cache_dir: cfg.zebra_db_path.clone(), - ephemeral: false, - delete_old_database: true, - debug_stop_at_height: None, - debug_validity_check_interval: None, - should_backup_non_finalized_state: true, - debug_skip_non_finalized_state_backup_task: false, - }, - validator_rpc_address: cfg - .validator_settings - .validator_jsonrpc_listen_address - .clone(), + common: build_common(cfg), + validator_state_config, validator_grpc_address, - validator_cookie_auth: cfg.validator_settings.validator_cookie_path.is_some(), - validator_cookie_path: cfg.validator_settings.validator_cookie_path, - validator_rpc_user: cfg - .validator_settings - .validator_user - .unwrap_or_else(|| "xxxxxx".to_string()), - validator_rpc_password: cfg - .validator_settings - .validator_password - .unwrap_or_else(|| "xxxxxx".to_string()), - service: cfg.service, - storage: cfg.storage, - network: cfg.network, - donation_address: cfg.donation_address, - indexer_version: env!("CARGO_PKG_VERSION").to_string(), + validator_cookie_auth, }) } } @@ -389,25 +377,31 @@ impl TryFrom for FetchServiceConfig { fn try_from(cfg: ZainodConfig) -> Result { Ok(FetchServiceConfig { - validator_rpc_address: cfg.validator_settings.validator_jsonrpc_listen_address, - validator_cookie_path: cfg.validator_settings.validator_cookie_path, - validator_rpc_user: cfg - .validator_settings - .validator_user - .unwrap_or_else(|| "xxxxxx".to_string()), - validator_rpc_password: cfg - .validator_settings - .validator_password - .unwrap_or_else(|| "xxxxxx".to_string()), - service: cfg.service, - storage: cfg.storage, - network: cfg.network, - donation_address: cfg.donation_address, - indexer_version: env!("CARGO_PKG_VERSION").to_string(), + common: build_common(cfg), }) } } +fn build_common(cfg: ZainodConfig) -> CommonBackendConfig { + CommonBackendConfig { + validator_rpc_address: cfg.validator_settings.validator_jsonrpc_listen_address, + validator_cookie_path: cfg.validator_settings.validator_cookie_path, + validator_rpc_user: cfg + .validator_settings + .validator_user + .unwrap_or_else(|| "xxxxxx".to_string()), + validator_rpc_password: cfg + .validator_settings + .validator_password + .unwrap_or_else(|| "xxxxxx".to_string()), + service: cfg.service, + storage: cfg.storage, + network: cfg.network, + donation_address: cfg.donation_address, + indexer_version: env!("CARGO_PKG_VERSION").to_string(), + } +} + #[cfg(test)] mod tests { use super::*; @@ -1056,10 +1050,39 @@ listen_address = "127.0.0.1:8137" let state_cfg = StateServiceConfig::try_from(cfg.clone()) .expect("StateServiceConfig conversion should succeed for default ZainodConfig"); - assert_eq!(state_cfg.indexer_version, env!("CARGO_PKG_VERSION")); + assert_eq!(state_cfg.common.indexer_version, env!("CARGO_PKG_VERSION")); let fetch_cfg = FetchServiceConfig::try_from(cfg) .expect("FetchServiceConfig conversion should succeed for default ZainodConfig"); - assert_eq!(fetch_cfg.indexer_version, env!("CARGO_PKG_VERSION")); + assert_eq!(fetch_cfg.common.indexer_version, env!("CARGO_PKG_VERSION")); + } + + /// `StateServiceConfig::try_from` and `FetchServiceConfig::try_from` + /// share a single `build_common` helper, so the two backends can + /// never quietly disagree on the common payload they hand to a + /// service. Locks that property in across every field: a future + /// hand-rolled divergence (e.g. one path stops applying the + /// missing-credentials sentinel, or a new common field gets + /// populated on only one side) makes this fail. Pretty-Debug + /// equality is used because not every constituent of + /// `CommonBackendConfig` derives `PartialEq`, and a single + /// stringified compare future-proofs the test against fields added + /// later. + #[test] + #[allow(deprecated)] + fn state_and_fetch_common_payloads_agree() { + let _guard = EnvGuard::new(); + + let cfg = ZainodConfig::default(); + + let state_cfg = StateServiceConfig::try_from(cfg.clone()) + .expect("StateServiceConfig conversion should succeed for default ZainodConfig"); + let fetch_cfg = FetchServiceConfig::try_from(cfg) + .expect("FetchServiceConfig conversion should succeed for default ZainodConfig"); + + assert_eq!( + format!("{:#?}", state_cfg.common), + format!("{:#?}", fetch_cfg.common), + ); } }