diff --git a/crates/zakura-consensus/Cargo.toml b/crates/zakura-consensus/Cargo.toml index 908197e252..ae4cf3dd92 100644 --- a/crates/zakura-consensus/Cargo.toml +++ b/crates/zakura-consensus/Cargo.toml @@ -30,6 +30,7 @@ progress-bar = [ ] # Test-only features +internal-bench = [] proptest-impl = ["proptest", "proptest-derive", "zakura-chain/proptest-impl", "zakura-state/proptest-impl"] [dependencies] @@ -114,5 +115,10 @@ harness = false name = "worst_case_tx_verification" harness = false +[[bench]] +name = "state_read_routing" +harness = false +required-features = ["internal-bench"] + [lints] workspace = true diff --git a/crates/zakura-consensus/benches/state_read_routing.rs b/crates/zakura-consensus/benches/state_read_routing.rs new file mode 100644 index 0000000000..94383bac34 --- /dev/null +++ b/crates/zakura-consensus/benches/state_read_routing.rs @@ -0,0 +1,61 @@ +//! Unloaded latency benchmark for transaction-verifier state read routing. + +#![allow(clippy::print_stdout)] + +use std::{env, time::Duration}; + +const DEFAULT_REQUESTS_PER_SAMPLE: usize = 100_000; +const DEFAULT_SAMPLES: usize = 10; + +fn main() { + let requests = env_usize( + "ZAKURA_STATE_READ_ROUTING_REQUESTS", + DEFAULT_REQUESTS_PER_SAMPLE, + ); + let samples = env_usize("ZAKURA_STATE_READ_ROUTING_SAMPLES", DEFAULT_SAMPLES); + assert!(samples > 0, "the benchmark needs at least one sample"); + + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build() + .expect("the benchmark runtime builds"); + + let mut buffered = Vec::with_capacity(samples); + let mut direct = Vec::with_capacity(samples); + for sample in 0..samples { + let (buffered_elapsed, direct_elapsed) = runtime.block_on( + zakura_consensus::router::benchmark_transaction_state_read_routing( + requests, + sample % 2 == 0, + ), + ); + buffered.push(buffered_elapsed); + direct.push(direct_elapsed); + } + + print_result("buffered_state_read", requests, buffered); + print_result("direct_state_read", requests, direct); +} + +fn print_result(operation: &str, requests: usize, mut timings: Vec) { + timings.sort_unstable(); + let median = timings[timings.len() / 2]; + let p95 = timings[timings.len().saturating_sub(1) * 95 / 100]; + let requests = u32::try_from(requests).expect("the benchmark request count fits in u32"); + let median_ns_per_request = median.as_secs_f64() * 1_000_000_000.0 / f64::from(requests); + let p95_ns_per_request = p95.as_secs_f64() * 1_000_000_000.0 / f64::from(requests); + + println!( + "operation={operation} samples={} requests_per_sample={requests} \ + median_ns_per_request={median_ns_per_request:.1} \ + p95_ns_per_request={p95_ns_per_request:.1}", + timings.len(), + ); +} + +fn env_usize(name: &str, default: usize) -> usize { + env::var(name) + .map(|value| value.parse().expect("benchmark setting must be a usize")) + .unwrap_or(default) +} diff --git a/crates/zakura-consensus/src/router.rs b/crates/zakura-consensus/src/router.rs index 7ed114d15f..c0e8433b10 100644 --- a/crates/zakura-consensus/src/router.rs +++ b/crates/zakura-consensus/src/router.rs @@ -92,6 +92,136 @@ where block: SemanticBlockVerifier, } +/// Routes transaction-verifier reads around the serialized read-write state buffer. +#[derive(Clone, Debug)] +struct TransactionStateRouter { + state: S, + read_state: R, +} + +impl TransactionStateRouter { + fn new(state: S, read_state: R) -> Self { + Self { state, read_state } + } +} + +impl Service for TransactionStateRouter +where + S: Service + Send + Clone + 'static, + S::Future: Send + 'static, + R: Service + + Send + + Clone + + 'static, + R::Future: Send + 'static, +{ + type Response = zs::Response; + type Error = BoxError; + type Future = + Pin> + Send + 'static>>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + // `call` selects the backing service from the request. Each selected service applies its + // own readiness and backpressure inside the returned future. + Poll::Ready(Ok(())) + } + + fn call(&mut self, request: zs::Request) -> Self::Future { + let read_request = match request { + zs::Request::CheckBestChainTipNullifiersAndAnchors(transaction) => { + zs::ReadRequest::CheckBestChainTipNullifiersAndAnchors(transaction) + } + zs::Request::BestChainNextMedianTimePast => { + zs::ReadRequest::BestChainNextMedianTimePast + } + zs::Request::UnspentBestChainUtxo(outpoint) => { + zs::ReadRequest::UnspentBestChainUtxo(outpoint) + } + request => return self.state.clone().oneshot(request).boxed(), + }; + + let read_state = self.read_state.clone(); + async move { + let response = read_state.oneshot(read_request).await?; + response.try_into().map_err(BoxError::from) + } + .boxed() + } +} + +/// Measures the unloaded latency of a read through the old buffered route and +/// the direct transaction-state route. +/// +/// This helper is public only so the `state_read_routing` benchmark can use the +/// production router without exposing its implementation type. +#[cfg(feature = "internal-bench")] +#[doc(hidden)] +pub async fn benchmark_transaction_state_read_routing( + requests: usize, + buffered_first: bool, +) -> (std::time::Duration, std::time::Duration) { + use zakura_chain::serialization::DateTime32; + + assert!(requests > 0, "the benchmark needs at least one request"); + + let write_state = tower::service_fn(|request: zs::Request| async move { + Err::( + format!("unexpected write-state benchmark request: {request:?}").into(), + ) + }); + let read_state = tower::service_fn(|request: zs::ReadRequest| async move { + match request { + zs::ReadRequest::BestChainNextMedianTimePast => Ok::<_, BoxError>( + zs::ReadResponse::BestChainNextMedianTimePast(DateTime32::MIN), + ), + request => Err(format!("unexpected read-state benchmark request: {request:?}").into()), + } + }); + + let direct = TransactionStateRouter::new(write_state, read_state); + let buffered = Buffer::new(BoxService::new(direct.clone()), 1); + + direct + .clone() + .oneshot(zs::Request::BestChainNextMedianTimePast) + .await + .expect("the direct benchmark warmup succeeds"); + buffered + .clone() + .oneshot(zs::Request::BestChainNextMedianTimePast) + .await + .expect("the buffered benchmark warmup succeeds"); + + async fn measure(mut service: S, requests: usize) -> std::time::Duration + where + S: Service, + S::Future: Send, + { + let start = std::time::Instant::now(); + for _ in 0..requests { + let response = service + .ready() + .await + .expect("the benchmark route becomes ready") + .call(zs::Request::BestChainNextMedianTimePast) + .await + .expect("the benchmark route answers the read request"); + std::hint::black_box(response); + } + start.elapsed() + } + + if buffered_first { + let buffered_elapsed = measure(buffered, requests).await; + let direct_elapsed = measure(direct, requests).await; + (buffered_elapsed, direct_elapsed) + } else { + let direct_elapsed = measure(direct, requests).await; + let buffered_elapsed = measure(buffered, requests).await; + (buffered_elapsed, direct_elapsed) + } +} + /// An error while semantically verifying a block. // // One or both of these error variants are at least 140 bytes @@ -258,11 +388,12 @@ where /// Block and transaction verification requests should be wrapped in a timeout, /// so that out-of-order and invalid requests do not hang indefinitely. /// See the [`router`](`crate::router`) module documentation for details. -#[instrument(skip(state_service, mempool))] -pub async fn init( +#[instrument(skip(state_service, transaction_state, mempool))] +async fn init_with_transaction_state( config: Config, network: &Network, mut state_service: S, + transaction_state: TransactionState, mempool: oneshot::Receiver, ) -> ( Buffer, Request>, @@ -276,6 +407,9 @@ pub async fn init( where S: Service + Send + Clone + 'static, S::Future: Send + 'static, + TransactionState: + Service + Send + Clone + 'static, + TransactionState::Future: Send + 'static, Mempool: Service + Send + Clone @@ -367,7 +501,7 @@ where // transaction verification - let transaction = transaction::Verifier::new(network, state_service.clone(), mempool); + let transaction = transaction::Verifier::new(network, transaction_state, mempool); let transaction = Buffer::new(BoxService::new(transaction), VERIFIER_BUFFER_BOUND); // block verification @@ -407,6 +541,70 @@ where (router, transaction, task_handles, max_checkpoint_height) } +/// Initializes block and transaction verification with one shared state service. +/// +/// Use [`init_with_read_state`] when a separate read service is available. +pub async fn init( + config: Config, + network: &Network, + state_service: S, + mempool: oneshot::Receiver, +) -> ( + Buffer, Request>, + Buffer< + BoxService, + transaction::Request, + >, + BackgroundTaskHandles, + Height, +) +where + S: Service + Send + Clone + 'static, + S::Future: Send + 'static, + Mempool: Service + + Send + + Clone + + 'static, + Mempool::Future: Send + 'static, +{ + let transaction_state = state_service.clone(); + init_with_transaction_state(config, network, state_service, transaction_state, mempool).await +} + +/// Initializes verification and routes transaction read-only queries through `read_state_service`. +pub async fn init_with_read_state( + config: Config, + network: &Network, + state_service: S, + read_state_service: R, + mempool: oneshot::Receiver, +) -> ( + Buffer, Request>, + Buffer< + BoxService, + transaction::Request, + >, + BackgroundTaskHandles, + Height, +) +where + S: Service + Send + Clone + 'static, + S::Future: Send + 'static, + R: Service + + Send + + Clone + + 'static, + R::Future: Send + 'static, + Mempool: Service + + Send + + Clone + + 'static, + Mempool::Future: Send + 'static, +{ + let transaction_state = TransactionStateRouter::new(state_service.clone(), read_state_service); + init_with_transaction_state(config, network, state_service, transaction_state, mempool).await +} + /// Parses the checkpoint list for `network` and `config`. /// Returns the checkpoint list and maximum checkpoint height. pub fn init_checkpoint_list(config: Config, network: &Network) -> (Arc, Height) { diff --git a/crates/zakura-consensus/src/router/tests.rs b/crates/zakura-consensus/src/router/tests.rs index a5077fd9e0..63abddf6f7 100644 --- a/crates/zakura-consensus/src/router/tests.rs +++ b/crates/zakura-consensus/src/router/tests.rs @@ -17,6 +17,94 @@ use zakura_test::transcript::{ExpectedTranscriptError, Transcript}; use super::*; +#[tokio::test] +async fn transaction_state_router_separates_reads_from_waiting_state_requests() { + use std::sync::atomic::{AtomicUsize, Ordering}; + + use tower::service_fn; + use zakura_chain::{serialization::DateTime32, transaction, transparent}; + + let write_calls = Arc::new(AtomicUsize::new(0)); + let write_state = service_fn({ + let write_calls = write_calls.clone(); + move |_request: zs::Request| { + write_calls.fetch_add(1, Ordering::SeqCst); + async move { Err::("write-state sentinel".into()) } + } + }); + + let read_calls = Arc::new(AtomicUsize::new(0)); + let read_state = service_fn({ + let read_calls = read_calls.clone(); + move |request: zs::ReadRequest| { + read_calls.fetch_add(1, Ordering::SeqCst); + async move { + match request { + zs::ReadRequest::BestChainNextMedianTimePast => Ok::<_, BoxError>( + zs::ReadResponse::BestChainNextMedianTimePast(DateTime32::MIN), + ), + zs::ReadRequest::CheckBestChainTipNullifiersAndAnchors(_) => { + Ok(zs::ReadResponse::ValidBestChainTipNullifiersAndAnchors) + } + zs::ReadRequest::UnspentBestChainUtxo(_) => { + Ok(zs::ReadResponse::UnspentBestChainUtxo(None)) + } + request => panic!("unexpected direct read request: {request:?}"), + } + } + } + }); + + let router = TransactionStateRouter::new(write_state, read_state); + let response = router + .clone() + .oneshot(zs::Request::BestChainNextMedianTimePast) + .await + .expect("the read service answers the direct query"); + assert_eq!( + response, + zs::Response::BestChainNextMedianTimePast(DateTime32::MIN) + ); + + let block: Block = zakura_test::vectors::BLOCK_MAINNET_GENESIS_BYTES + .zcash_deserialize_into() + .expect("the genesis block is valid"); + let transaction = transaction::UnminedTx::from(block.transactions[0].clone()); + let response = router + .clone() + .oneshot(zs::Request::CheckBestChainTipNullifiersAndAnchors( + transaction, + )) + .await + .expect("the read service checks nullifiers and anchors"); + assert_eq!( + response, + zs::Response::ValidBestChainTipNullifiersAndAnchors + ); + + let outpoint = transparent::OutPoint { + hash: transaction::Hash([0; 32]), + index: 0, + }; + let response = router + .clone() + .oneshot(zs::Request::UnspentBestChainUtxo(outpoint)) + .await + .expect("the read service checks the best-chain UTXO set"); + assert_eq!(response, zs::Response::UnspentBestChainUtxo(None)); + + assert_eq!(read_calls.load(Ordering::SeqCst), 3); + assert_eq!(write_calls.load(Ordering::SeqCst), 0); + + let error = router + .oneshot(zs::Request::AwaitUtxo(outpoint)) + .await + .expect_err("the write-state sentinel rejects the waiting query"); + assert!(error.to_string().contains("write-state sentinel")); + assert_eq!(read_calls.load(Ordering::SeqCst), 3); + assert_eq!(write_calls.load(Ordering::SeqCst), 1); +} + /// The timeout we apply to each verify future during testing. /// /// The checkpoint verifier uses `tokio::sync::oneshot` channels as futures. diff --git a/crates/zakurad/src/commands/start.rs b/crates/zakurad/src/commands/start.rs index ceead5d8c5..33758e27ba 100644 --- a/crates/zakurad/src/commands/start.rs +++ b/crates/zakurad/src/commands/start.rs @@ -542,10 +542,11 @@ impl StartCmd { info!("initializing verifiers"); let (tx_verifier_setup_tx, tx_verifier_setup_rx) = oneshot::channel(); let (block_verifier_router, tx_verifier, consensus_task_handles, max_checkpoint_height) = - zakura_consensus::router::init( + zakura_consensus::router::init_with_read_state( config.consensus.clone(), &config.network.network, state.clone(), + read_only_state_service.clone(), tx_verifier_setup_rx, ) .await; @@ -602,6 +603,7 @@ impl StartCmd { config.network.expose_peer_addresses, peer_set.clone(), state.clone(), + tower::util::BoxCloneService::new(read_only_state_service.clone()), tx_verifier, sync_status.clone(), latest_chain_tip.clone(), @@ -1143,6 +1145,7 @@ impl StartCmd { /// based on the configurations of the services that use the state concurrently. fn state_buffer_bound(config: &ZakuradConfig) -> usize { // Ignore the checkpoint verify limit, because it is very large. + // Mempool read traffic bypasses this buffer. // // TODO: do we also need to account for concurrent use across services? // we could multiply the maximum by 3/2, or add a fixed constant @@ -1150,7 +1153,6 @@ impl StartCmd { config.sync.download_concurrency_limit, config.sync.full_verify_concurrency_limit, inbound::downloads::MAX_INBOUND_CONCURRENCY, - mempool::downloads::MAX_INBOUND_CONCURRENCY, ] .into_iter() .max() diff --git a/crates/zakurad/src/components/inbound/tests/fake_peer_set.rs b/crates/zakurad/src/components/inbound/tests/fake_peer_set.rs index 7634889dad..05046f85b4 100644 --- a/crates/zakurad/src/components/inbound/tests/fake_peer_set.rs +++ b/crates/zakurad/src/components/inbound/tests/fake_peer_set.rs @@ -1332,7 +1332,7 @@ async fn setup_with_misbehavior_receiver( let (sync_status, mut recent_syncs) = SyncStatus::new(); // UTXO verification doesn't matter for these tests. - let (state, _read_only_state_service, latest_chain_tip, mut chain_tip_change) = + let (state, read_only_state_service, latest_chain_tip, mut chain_tip_change) = zakura_state::init(state_config.clone(), &network, Height::MAX, 0) .await .expect("ephemeral state initialization succeeds"); @@ -1414,6 +1414,7 @@ async fn setup_with_misbehavior_receiver( false, buffered_peer_set.clone(), state_service.clone(), + tower::util::BoxCloneService::new(read_only_state_service), buffered_tx_verifier.clone(), sync_status.clone(), latest_chain_tip.clone(), diff --git a/crates/zakurad/src/components/inbound/tests/real_peer_set.rs b/crates/zakurad/src/components/inbound/tests/real_peer_set.rs index c0e324b43d..2c5c36e345 100644 --- a/crates/zakurad/src/components/inbound/tests/real_peer_set.rs +++ b/crates/zakurad/src/components/inbound/tests/real_peer_set.rs @@ -962,7 +962,7 @@ async fn setup( // State // UTXO verification doesn't matter for these tests. - let (state_service, _read_only_state_service, latest_chain_tip, chain_tip_change) = + let (state_service, read_only_state_service, latest_chain_tip, chain_tip_change) = zakura_state::init(state_config, &network, Height::MAX, 0) .await .expect("ephemeral state initialization succeeds"); @@ -1030,6 +1030,7 @@ async fn setup( false, peer_set.clone(), state_service.clone(), + tower::util::BoxCloneService::new(read_only_state_service), buffered_tx_verifier.clone(), sync_status.clone(), latest_chain_tip.clone(), diff --git a/crates/zakurad/src/components/mempool.rs b/crates/zakurad/src/components/mempool.rs index 50dbbb1735..3852d7a840 100644 --- a/crates/zakurad/src/components/mempool.rs +++ b/crates/zakurad/src/components/mempool.rs @@ -28,7 +28,12 @@ use std::{ use futures::{future::FutureExt, stream::Stream}; use tokio::sync::{broadcast, mpsc, oneshot}; -use tower::{buffer::Buffer, timeout::Timeout, util::BoxService, Service}; +use tower::{ + buffer::Buffer, + timeout::Timeout, + util::{BoxCloneService, BoxService}, + Service, +}; use zakura_chain::{ block::{self, Height}, @@ -110,11 +115,12 @@ fn transaction_error_peer_log_label( type Outbound = Buffer, zn::Request>; type State = Buffer, zs::Request>; +type ReadState = BoxCloneService; type TxVerifier = Buffer< BoxService, transaction::Request, >; -type InboundTxDownloads = TxDownloads, Timeout, State>; +type InboundTxDownloads = TxDownloads, Timeout, ReadState>; fn transaction_misbehavior( error: &TransactionDownloadVerifyError, @@ -286,9 +292,13 @@ pub struct Mempool { /// Used to construct the transaction downloader. outbound: Outbound, - /// Handle to the state service. + /// Handle to the read state service. /// Used to construct the transaction downloader. - state: State, + read_state: ReadState, + + /// Keeps the read-write state service alive for the mempool lifetime. + /// The transaction downloader does not send requests through this handle. + _state_guard: State, /// Handle to the transaction verifier service. /// Used to construct the transaction downloader. @@ -331,6 +341,7 @@ impl Mempool { expose_peer_addresses: bool, outbound: Outbound, state: State, + read_state: ReadState, tx_verifier: TxVerifier, sync_status: SyncStatus, latest_chain_tip: zs::LatestChainTip, @@ -350,7 +361,8 @@ impl Mempool { latest_chain_tip, chain_tip_change, outbound, - state, + read_state, + _state_guard: state, tx_verifier, transaction_sender, misbehavior_sender, @@ -423,7 +435,7 @@ impl Mempool { let tx_downloads = Box::pin(TxDownloads::new( Timeout::new(self.outbound.clone(), TRANSACTION_DOWNLOAD_TIMEOUT), Timeout::new(self.tx_verifier.clone(), TRANSACTION_VERIFY_TIMEOUT), - self.state.clone(), + self.read_state.clone(), self.expose_peer_addresses, self.config.max_transaction_bytes, )); diff --git a/crates/zakurad/src/components/mempool/downloads.rs b/crates/zakurad/src/components/mempool/downloads.rs index d0b6f51675..17ae5e7961 100644 --- a/crates/zakurad/src/components/mempool/downloads.rs +++ b/crates/zakurad/src/components/mempool/downloads.rs @@ -176,7 +176,10 @@ where ZN::Future: Send, ZV: Service + Send + Clone + 'static, ZV::Future: Send, - ZS: Service + Send + Clone + 'static, + ZS: Service + + Send + + Clone + + 'static, ZS::Future: Send, { // Services @@ -187,7 +190,7 @@ where /// A service that verifies downloaded transactions. verifier: ZV, - /// A service that manages cached blockchain state. + /// A service that reads cached blockchain state. state: ZS, /// Whether legacy peer address labels in logs are unredacted. @@ -243,7 +246,10 @@ where ZN::Future: Send, ZV: Service + Send + Clone + 'static, ZV::Future: Send, - ZS: Service + Send + Clone + 'static, + ZS: Service + + Send + + Clone + + 'static, ZS::Future: Send, { type Item = Result< @@ -321,7 +327,10 @@ where ZN::Future: Send, ZV: Service + Send + Clone + 'static, ZV::Future: Send, - ZS: Service + Send + Clone + 'static, + ZS: Service + + Send + + Clone + + 'static, ZS::Future: Send, { /// Initialize a new download stream with the provided services. @@ -443,9 +452,9 @@ where trace!(?txid, "transaction is not in best chain"); - let (tip_height, next_height) = match state.oneshot(zs::Request::Tip).await { - Ok(zs::Response::Tip(None)) => Ok((None, Height(0))), - Ok(zs::Response::Tip(Some((height, _hash)))) => { + let (tip_height, next_height) = match state.oneshot(zs::ReadRequest::Tip).await { + Ok(zs::ReadResponse::Tip(None)) => Ok((None, Height(0))), + Ok(zs::ReadResponse::Tip(Some((height, _hash)))) => { let next_height = (height + 1).expect("valid heights are far below the maximum"); Ok((Some(height), next_height)) @@ -706,11 +715,13 @@ where .await .map_err(CloneError::from) .map_err(TransactionDownloadVerifyError::StateError)? - .call(zs::Request::Transaction(txid.mined_id())) + .call(zs::ReadRequest::Transaction(txid.mined_id())) .await { - Ok(zs::Response::Transaction(None)) => Ok(()), - Ok(zs::Response::Transaction(Some(_))) => Err(TransactionDownloadVerifyError::InState), + Ok(zs::ReadResponse::Transaction(None)) => Ok(()), + Ok(zs::ReadResponse::Transaction(Some(_))) => { + Err(TransactionDownloadVerifyError::InState) + } Ok(_) => unreachable!("wrong response"), Err(e) => Err(TransactionDownloadVerifyError::StateError(e.into())), }?; @@ -726,7 +737,10 @@ where ZN::Future: Send, ZV: Service + Send + Clone + 'static, ZV::Future: Send, - ZS: Service + Send + Clone + 'static, + ZS: Service + + Send + + Clone + + 'static, ZS::Future: Send, { fn drop(mut self: Pin<&mut Self>) { @@ -751,7 +765,7 @@ mod tests { type PendingNetwork = BoxCloneService; type PendingVerifier = BoxCloneService; - type PendingState = BoxCloneService; + type PendingState = BoxCloneService; fn tx_id(index: u64) -> UnminedTxId { let mut bytes = [0; 32]; @@ -786,7 +800,7 @@ mod tests { future::pending::>() })), BoxCloneService::new(service_fn(|_request| { - future::pending::>() + future::pending::>() })), false, u64::MAX, @@ -852,8 +866,8 @@ mod tests { })), BoxCloneService::new(service_fn(|request| async move { match request { - zs::Request::Transaction(_) => Ok(zs::Response::Transaction(None)), - zs::Request::Tip => Ok(zs::Response::Tip(None)), + zs::ReadRequest::Transaction(_) => Ok(zs::ReadResponse::Transaction(None)), + zs::ReadRequest::Tip => Ok(zs::ReadResponse::Tip(None)), request => Err(format!("unexpected state request: {request:?}").into()), } })), @@ -902,8 +916,8 @@ mod tests { })), BoxCloneService::new(service_fn(|request| async move { match request { - zs::Request::Transaction(_) => Ok(zs::Response::Transaction(None)), - zs::Request::Tip => Ok(zs::Response::Tip(None)), + zs::ReadRequest::Transaction(_) => Ok(zs::ReadResponse::Transaction(None)), + zs::ReadRequest::Tip => Ok(zs::ReadResponse::Tip(None)), request => Err(format!("unexpected state request: {request:?}").into()), } })), @@ -961,8 +975,8 @@ mod tests { })), BoxCloneService::new(service_fn(|request| async move { match request { - zs::Request::Transaction(_) => Ok(zs::Response::Transaction(None)), - zs::Request::Tip => Ok(zs::Response::Tip(None)), + zs::ReadRequest::Transaction(_) => Ok(zs::ReadResponse::Transaction(None)), + zs::ReadRequest::Tip => Ok(zs::ReadResponse::Tip(None)), request => Err(format!("unexpected state request: {request:?}").into()), } })), @@ -1097,8 +1111,8 @@ mod tests { })), BoxCloneService::new(service_fn(|request| async move { match request { - zs::Request::Transaction(_) => Ok(zs::Response::Transaction(None)), - zs::Request::Tip => Ok(zs::Response::Tip(None)), + zs::ReadRequest::Transaction(_) => Ok(zs::ReadResponse::Transaction(None)), + zs::ReadRequest::Tip => Ok(zs::ReadResponse::Tip(None)), request => Err(format!("unexpected state request: {request:?}").into()), } })), @@ -1153,8 +1167,8 @@ mod tests { })), BoxCloneService::new(service_fn(|request| async move { match request { - zs::Request::Transaction(_) => Ok(zs::Response::Transaction(None)), - zs::Request::Tip => Ok(zs::Response::Tip(None)), + zs::ReadRequest::Transaction(_) => Ok(zs::ReadResponse::Transaction(None)), + zs::ReadRequest::Tip => Ok(zs::ReadResponse::Tip(None)), request => Err(format!("unexpected state request: {request:?}").into()), } })), diff --git a/crates/zakurad/src/components/mempool/tests/prop.rs b/crates/zakurad/src/components/mempool/tests/prop.rs index 45da187b85..9d5be1ecb4 100644 --- a/crates/zakurad/src/components/mempool/tests/prop.rs +++ b/crates/zakurad/src/components/mempool/tests/prop.rs @@ -9,7 +9,10 @@ use proptest_derive::Arbitrary; use chrono::Duration; use tokio::time; -use tower::{buffer::Buffer, util::BoxService}; +use tower::{ + buffer::Buffer, + util::{BoxCloneService, BoxService}, +}; use zakura_chain::{ block::{self, Block}, @@ -34,7 +37,10 @@ use crate::components::{ type MockPeerSet = MockService; /// A [`MockService`] representing the Zebra state service. -type MockState = MockService; +type MockState = MockService; + +/// A [`MockService`] representing the retained read-write state service. +type MockStateGuard = MockService; /// A [`MockService`] representing the Zebra transaction verifier service. type MockTxVerifier = MockService; @@ -72,6 +78,7 @@ proptest! { mut mempool, _peer_set, _state_service, + _state_guard, _tx_verifier, mut recent_syncs, mut chain_tip_sender, @@ -122,6 +129,7 @@ proptest! { mut mempool, _peer_set, _state_service, + _state_guard, _tx_verifier, mut recent_syncs, mut chain_tip_sender, @@ -207,6 +215,7 @@ proptest! { mut mempool, mut peer_set, mut state_service, + mut state_guard, mut tx_verifier, mut recent_syncs, _chain_tip_sender, @@ -239,6 +248,7 @@ proptest! { peer_set.expect_no_requests().await?; state_service.expect_no_requests().await?; + state_guard.expect_no_requests().await?; tx_verifier.expect_no_requests().await?; Ok(()) @@ -261,12 +271,14 @@ fn setup( Mempool, MockPeerSet, MockState, + MockStateGuard, MockTxVerifier, RecentSyncLengths, ChainTipSender, ) { let peer_set = MockService::build().for_prop_tests(); let state_service = MockService::build().for_prop_tests(); + let state_guard = MockService::build().for_prop_tests(); let tx_verifier = MockService::build().for_prop_tests(); let (sync_status, recent_syncs) = SyncStatus::new(); @@ -281,7 +293,8 @@ fn setup( }, false, Buffer::new(BoxService::new(peer_set.clone()), 1), - Buffer::new(BoxService::new(state_service.clone()), 1), + Buffer::new(BoxService::new(state_guard.clone()), 1), + BoxCloneService::new(state_service.clone()), Buffer::new(BoxService::new(tx_verifier.clone()), 1), sync_status, latest_chain_tip, @@ -299,6 +312,7 @@ fn setup( mempool, peer_set, state_service, + state_guard, tx_verifier, recent_syncs, chain_tip_sender, diff --git a/crates/zakurad/src/components/mempool/tests/vector.rs b/crates/zakurad/src/components/mempool/tests/vector.rs index 45fcffaba7..c8b94a5244 100644 --- a/crates/zakurad/src/components/mempool/tests/vector.rs +++ b/crates/zakurad/src/components/mempool/tests/vector.rs @@ -6,7 +6,7 @@ use std::{sync::Arc, time::Duration}; use color_eyre::Report; use tokio::time::{self, timeout}; -use tower::{ServiceBuilder, ServiceExt}; +use tower::{util::BoxCloneService, ServiceBuilder, ServiceExt}; use rand::{seq::SliceRandom, thread_rng}; use zakura_chain::{ @@ -2704,7 +2704,7 @@ async fn setup_with_mempool_config_and_misbehavior_sender( // UTXO verification doesn't matter here. let state_config = StateConfig::ephemeral(); - let (state, _read_only_state_service, latest_chain_tip, mut chain_tip_change) = + let (state, read_only_state_service, latest_chain_tip, mut chain_tip_change) = zakura_state::init(state_config, network, Height::MAX, 0) .await .expect("ephemeral state initialization succeeds"); @@ -2718,6 +2718,7 @@ async fn setup_with_mempool_config_and_misbehavior_sender( false, Buffer::new(BoxService::new(peer_set.clone()), 1), state_service.clone(), + BoxCloneService::new(read_only_state_service), Buffer::new(BoxService::new(tx_verifier.clone()), 1), sync_status, latest_chain_tip, @@ -2784,7 +2785,7 @@ async fn cancel_handles_drained_after_verification_timeout() { let _init_guard = zakura_test::init(); let peer_set: MockPeerSet = MockService::build().for_unit_tests(); - let state: MockService = + let state: MockService = MockService::build().for_unit_tests(); let tx_verifier: MockTxVerifier = MockService::build().for_unit_tests(); diff --git a/docs/changelog/unreleased/784.md b/docs/changelog/unreleased/784.md new file mode 100644 index 0000000000..4b63f00765 --- /dev/null +++ b/docs/changelog/unreleased/784.md @@ -0,0 +1,5 @@ +## Changed + +- Zakura now routes transaction-verifier and mempool state reads around the + serialized read-write state buffer + ([#784](https://github.com/zakura-core/zakura/pull/784)).