1use std::marker::PhantomData;
29use std::ops::{Deref, DerefMut};
30use std::str::FromStr;
31use std::time::Duration;
32
33use http::header::ACCEPT;
34use miden_node_tracing::grpc::OtelInterceptor;
35use miden_node_tracing::{debug, info, warn};
36use miden_protocol::Word;
37use miden_protocol::batch::ProposedBatch;
38use tonic::metadata::AsciiMetadataValue;
39use tonic::service::interceptor::InterceptedService;
40use tonic::transport::{Channel, ClientTlsConfig, Endpoint, Error as TransportError};
41use tonic::{Request, Status};
42use url::Url;
43
44use crate::generated;
45
46#[derive(Clone)]
47pub struct Interceptor {
48 otel: Option<OtelInterceptor>,
49 accept: AsciiMetadataValue,
50 auth_header_value: Option<AsciiMetadataValue>,
51}
52
53impl Default for Interceptor {
54 fn default() -> Self {
55 Self {
56 otel: None,
57 accept: AsciiMetadataValue::from_static(Self::MEDIA_TYPE),
58 auth_header_value: None,
59 }
60 }
61}
62
63impl Interceptor {
64 const MEDIA_TYPE: &str = "application/vnd.miden";
65 const VERSION: &str = "version";
66 const GENESIS: &str = "genesis";
67 const NETWORK_TX_AUTH_HEADER_NAME: &str = "x-miden-network-tx-auth";
68
69 fn new(
70 enable_otel: bool,
71 version: Option<&str>,
72 genesis: Option<&str>,
73 auth_header: Option<AsciiMetadataValue>,
74 ) -> Self {
75 if let Some(version) = version
76 && !version.is_ascii()
77 {
78 panic!("version contains non-ascii values: {version}");
79 }
80
81 if let Some(genesis) = genesis
82 && !genesis.is_ascii()
83 {
84 panic!("genesis contains non-ascii values: {genesis}");
85 }
86
87 let accept = match (version, genesis) {
88 (None, None) => Self::MEDIA_TYPE.to_string(),
89 (None, Some(genesis)) => format!("{}; {}={genesis}", Self::MEDIA_TYPE, Self::GENESIS),
90 (Some(version), None) => format!("{}; {}={version}", Self::MEDIA_TYPE, Self::VERSION),
91 (Some(version), Some(genesis)) => format!(
92 "{}; {}={version}; {}={genesis}",
93 Self::MEDIA_TYPE,
94 Self::VERSION,
95 Self::GENESIS
96 ),
97 };
98 Self {
99 otel: enable_otel.then_some(OtelInterceptor),
100 accept: AsciiMetadataValue::from_str(&accept).unwrap(),
102 auth_header_value: auth_header,
103 }
104 }
105}
106
107impl tonic::service::Interceptor for Interceptor {
108 fn call(&mut self, mut request: tonic::Request<()>) -> Result<Request<()>, Status> {
109 if let Some(mut otel) = self.otel {
110 request = otel.call(request)?;
111 }
112
113 if request.metadata().get(ACCEPT.as_str()).is_none() {
114 request.metadata_mut().insert(ACCEPT.as_str(), self.accept.clone());
115 }
116
117 if let Some(value) = &self.auth_header_value {
118 request.metadata_mut().insert(Self::NETWORK_TX_AUTH_HEADER_NAME, value.clone());
119 }
120
121 Ok(request)
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn interceptor_preserves_existing_accept_metadata() {
131 let original_accept =
132 AsciiMetadataValue::from_static("application/vnd.miden; version=1.2; genesis=0x1234");
133 let mut request = Request::new(());
134 request.metadata_mut().insert(ACCEPT.as_str(), original_accept.clone());
135
136 let mut interceptor = Interceptor::new(false, Some("9.9"), Some("0xabcd"), None);
137 let request = tonic::service::Interceptor::call(&mut interceptor, request)
138 .expect("interceptor should succeed");
139
140 assert_eq!(request.metadata().get(ACCEPT.as_str()), Some(&original_accept));
141 }
142
143 #[test]
144 fn interceptor_inserts_accept_metadata_when_missing() {
145 let mut interceptor = Interceptor::new(false, Some("9.9"), Some("0xabcd"), None);
146
147 let request = tonic::service::Interceptor::call(&mut interceptor, Request::new(()))
148 .expect("interceptor should succeed");
149
150 assert_eq!(
151 request.metadata().get(ACCEPT.as_str()).and_then(|value| value.to_str().ok()),
152 Some("application/vnd.miden; version=9.9; genesis=0xabcd"),
153 );
154 }
155
156 #[tokio::test]
157 async fn connection_monitor_stops_when_cancelled() {
158 let builder = Builder::new(Url::parse("http://127.0.0.1:1").unwrap())
159 .without_tls()
160 .without_timeout()
161 .without_metadata_version()
162 .without_metadata_genesis()
163 .without_auth_header()
164 .without_otel_context_injection();
165 let shutdown = miden_node_utils::shutdown::CancellationToken::new();
166 shutdown.cancel();
167
168 tokio::time::timeout(
169 Duration::from_millis(100),
170 builder.monitor::<RpcClient>("test-dependency", shutdown),
171 )
172 .await
173 .expect("cancelled monitor should return promptly");
174 }
175}
176
177type InterceptedChannel = InterceptedService<Channel, Interceptor>;
181type GeneratedRpcClient =
182 generated::miden::node::v1::node_service_client::NodeServiceClient<InterceptedChannel>;
183type GeneratedNoteTransportClient =
184 generated::miden::note_transport::v1::note_transport_service_client::NoteTransportServiceClient<
185 InterceptedChannel,
186 >;
187type GeneratedProxyStatusClient =
188 generated::miden::remote_prover::v1::proxy_status_service_client::ProxyStatusServiceClient<
189 InterceptedChannel,
190 >;
191type GeneratedProverClient =
192 generated::miden::remote_prover::v1::prover_service_client::ProverServiceClient<
193 InterceptedChannel,
194 >;
195type GeneratedValidatorClient =
196 generated::miden::validator::v1::validator_service_client::ValidatorServiceClient<
197 InterceptedChannel,
198 >;
199type GeneratedNtxBuilderClient =
200 generated::miden::ntx_builder::v1::network_transaction_builder_service_client::NetworkTransactionBuilderServiceClient<InterceptedChannel>;
201type GeneratedSequencerClient =
202 generated::miden::sequencer::v1::sequencer_service_client::SequencerServiceClient<
203 InterceptedChannel,
204 >;
205type GeneratedProvenTransaction = generated::submission::ProvenTransactionSubmission;
206type SealedTransactionInputs = generated::submission::SealedTransactionInputs;
207
208#[derive(Debug, Clone)]
212pub struct RpcClient(GeneratedRpcClient);
213#[derive(Debug, Clone)]
214pub struct NoteTransportClient(GeneratedNoteTransportClient);
215#[derive(Debug, Clone)]
216pub struct RemoteProverProxyStatusClient(GeneratedProxyStatusClient);
217#[derive(Debug, Clone)]
218pub struct RemoteProverClient(GeneratedProverClient);
219#[derive(Debug, Clone)]
220pub struct ValidatorClient(GeneratedValidatorClient);
221#[derive(Debug, Clone)]
222pub struct NtxBuilderClient(GeneratedNtxBuilderClient);
223#[derive(Debug, Clone)]
224pub struct SequencerClient(GeneratedSequencerClient);
225
226impl DerefMut for RpcClient {
227 fn deref_mut(&mut self) -> &mut Self::Target {
228 &mut self.0
229 }
230}
231
232impl Deref for RpcClient {
233 type Target = GeneratedRpcClient;
234
235 fn deref(&self) -> &Self::Target {
236 &self.0
237 }
238}
239
240impl DerefMut for RemoteProverProxyStatusClient {
241 fn deref_mut(&mut self) -> &mut Self::Target {
242 &mut self.0
243 }
244}
245
246impl Deref for RemoteProverProxyStatusClient {
247 type Target = GeneratedProxyStatusClient;
248
249 fn deref(&self) -> &Self::Target {
250 &self.0
251 }
252}
253
254impl DerefMut for RemoteProverClient {
255 fn deref_mut(&mut self) -> &mut Self::Target {
256 &mut self.0
257 }
258}
259
260impl Deref for RemoteProverClient {
261 type Target = GeneratedProverClient;
262
263 fn deref(&self) -> &Self::Target {
264 &self.0
265 }
266}
267
268impl DerefMut for ValidatorClient {
269 fn deref_mut(&mut self) -> &mut Self::Target {
270 &mut self.0
271 }
272}
273
274impl Deref for ValidatorClient {
275 type Target = GeneratedValidatorClient;
276
277 fn deref(&self) -> &Self::Target {
278 &self.0
279 }
280}
281
282impl DerefMut for NtxBuilderClient {
283 fn deref_mut(&mut self) -> &mut Self::Target {
284 &mut self.0
285 }
286}
287
288impl Deref for NtxBuilderClient {
289 type Target = GeneratedNtxBuilderClient;
290
291 fn deref(&self) -> &Self::Target {
292 &self.0
293 }
294}
295
296impl DerefMut for SequencerClient {
297 fn deref_mut(&mut self) -> &mut Self::Target {
298 &mut self.0
299 }
300}
301
302impl Deref for SequencerClient {
303 type Target = GeneratedSequencerClient;
304
305 fn deref(&self) -> &Self::Target {
306 &self.0
307 }
308}
309
310pub trait GrpcClient {
315 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self;
316}
317
318impl GrpcClient for RpcClient {
319 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
320 Self(GeneratedRpcClient::new(InterceptedService::new(channel, interceptor)))
321 }
322}
323
324impl Deref for NoteTransportClient {
325 type Target = GeneratedNoteTransportClient;
326
327 fn deref(&self) -> &Self::Target {
328 &self.0
329 }
330}
331
332impl DerefMut for NoteTransportClient {
333 fn deref_mut(&mut self) -> &mut Self::Target {
334 &mut self.0
335 }
336}
337
338impl GrpcClient for NoteTransportClient {
339 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
340 Self(GeneratedNoteTransportClient::new(InterceptedService::new(channel, interceptor)))
341 }
342}
343
344impl GrpcClient for RemoteProverProxyStatusClient {
345 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
346 Self(GeneratedProxyStatusClient::new(InterceptedService::new(channel, interceptor)))
347 }
348}
349
350impl GrpcClient for RemoteProverClient {
351 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
352 Self(GeneratedProverClient::new(InterceptedService::new(channel, interceptor)))
353 }
354}
355
356impl GrpcClient for ValidatorClient {
357 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
358 Self(GeneratedValidatorClient::new(InterceptedService::new(channel, interceptor)))
359 }
360}
361
362impl GrpcClient for NtxBuilderClient {
363 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
364 Self(GeneratedNtxBuilderClient::new(InterceptedService::new(channel, interceptor)))
365 }
366}
367
368impl GrpcClient for SequencerClient {
369 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
370 Self(GeneratedSequencerClient::new(InterceptedService::new(channel, interceptor)))
371 }
372}
373
374#[derive(Clone, Debug)]
405pub struct Builder<State> {
406 endpoint: Endpoint,
407 endpoint_url: Url,
408 metadata_version: Option<String>,
409 metadata_genesis: Option<Word>,
410 metadata_auth_header_value: Option<AsciiMetadataValue>,
411 enable_otel: bool,
412 _state: PhantomData<State>,
413}
414
415#[derive(Copy, Clone, Debug)]
416pub struct WantsTls;
417#[derive(Copy, Clone, Debug)]
418pub struct WantsTimeout;
419#[derive(Copy, Clone, Debug)]
420pub struct WantsVersion;
421#[derive(Copy, Clone, Debug)]
422pub struct WantsGenesis;
423#[derive(Copy, Clone, Debug)]
424pub struct WantsOTel;
425#[derive(Copy, Clone, Debug)]
426pub struct WantsConnection;
427
428impl<State> Builder<State> {
429 fn next_state<Next>(self) -> Builder<Next> {
431 Builder {
432 endpoint: self.endpoint,
433 endpoint_url: self.endpoint_url,
434 metadata_version: self.metadata_version,
435 metadata_genesis: self.metadata_genesis,
436 metadata_auth_header_value: self.metadata_auth_header_value,
437 enable_otel: self.enable_otel,
438 _state: PhantomData::<Next>,
439 }
440 }
441}
442
443const HTTP2_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(20);
445const HTTP2_KEEPALIVE_TIMEOUT: Duration = Duration::from_secs(10);
447const TCP_KEEPALIVE: Duration = Duration::from_secs(30);
449
450impl Builder<WantsTls> {
451 pub fn new(url: Url) -> Builder<WantsTls> {
454 let endpoint = Endpoint::from_shared(String::from(url.clone()))
455 .expect("Url type always results in valid endpoint")
456 .http2_keep_alive_interval(HTTP2_KEEPALIVE_INTERVAL)
459 .keep_alive_timeout(HTTP2_KEEPALIVE_TIMEOUT)
460 .keep_alive_while_idle(true)
461 .tcp_keepalive(Some(TCP_KEEPALIVE));
462
463 Builder {
464 endpoint,
465 endpoint_url: url,
466 metadata_version: None,
467 metadata_genesis: None,
468 metadata_auth_header_value: None,
469 enable_otel: false,
470 _state: PhantomData,
471 }
472 }
473
474 pub fn without_tls(self) -> Builder<WantsTimeout> {
476 self.next_state()
477 }
478
479 pub fn with_tls(mut self) -> Result<Builder<WantsTimeout>, TransportError> {
481 self.endpoint = self.endpoint.tls_config(ClientTlsConfig::new().with_native_roots())?;
482
483 Ok(self.next_state())
484 }
485}
486
487impl Builder<WantsTimeout> {
488 pub fn without_timeout(self) -> Builder<WantsVersion> {
490 self.next_state()
491 }
492
493 pub fn with_timeout(mut self, duration: Duration) -> Builder<WantsVersion> {
495 self.endpoint = self.endpoint.timeout(duration);
496 self.next_state()
497 }
498}
499
500impl Builder<WantsVersion> {
501 pub fn without_metadata_version(mut self) -> Builder<WantsGenesis> {
503 self.metadata_version = None;
504 self.next_state()
505 }
506
507 pub fn with_metadata_version(mut self, version: String) -> Builder<WantsGenesis> {
509 self.metadata_version = Some(version);
510 self.next_state()
511 }
512}
513
514impl Builder<WantsGenesis> {
515 pub fn without_metadata_genesis(mut self) -> Builder<WantsOTel> {
517 self.metadata_genesis = None;
518 self.next_state()
519 }
520
521 pub fn with_metadata_genesis(mut self, genesis: Word) -> Builder<WantsOTel> {
523 self.metadata_genesis = Some(genesis);
524 self.next_state()
525 }
526}
527
528impl Builder<WantsOTel> {
529 #[must_use]
531 pub fn without_auth_header(mut self) -> Self {
532 self.metadata_auth_header_value = None;
533 self
534 }
535
536 #[must_use]
538 pub fn with_auth_header_value(mut self, value: AsciiMetadataValue) -> Self {
539 self.metadata_auth_header_value = Some(value);
540 self
541 }
542
543 pub fn with_otel_context_injection(mut self) -> Builder<WantsConnection> {
548 self.enable_otel = true;
549 self.next_state()
550 }
551
552 pub fn without_otel_context_injection(mut self) -> Builder<WantsConnection> {
555 self.enable_otel = false;
556 self.next_state()
557 }
558}
559
560impl Builder<WantsConnection> {
561 pub async fn connect<T>(self) -> Result<T, TransportError>
563 where
564 T: GrpcClient,
565 {
566 let channel = self.endpoint.connect().await?;
567 Ok(self.connect_with_channel::<T>(channel))
568 }
569
570 pub fn connect_lazy<T>(self) -> T
572 where
573 T: GrpcClient,
574 {
575 let channel = self.endpoint.connect_lazy();
576 self.connect_with_channel::<T>(channel)
577 }
578
579 pub async fn monitor<T>(
585 self,
586 dependency_name: &'static str,
587 shutdown: miden_node_utils::shutdown::CancellationToken,
588 ) where
589 T: GrpcClient + Send + 'static,
590 {
591 const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
592 const RETRY_MIN: Duration = Duration::from_secs(1);
593 const RETRY_MAX: Duration = Duration::from_secs(30);
594
595 use miden_node_utils::retry::BackoffBuilder;
596
597 let endpoint = miden_node_utils::formatting::format_endpoint(&self.endpoint_url);
598 let mut backoff = miden_node_utils::retry::exponential(RETRY_MIN, RETRY_MAX).build();
599 let mut first_failure = true;
600
601 loop {
602 let attempt = tokio::time::timeout(CONNECT_TIMEOUT, self.clone().connect::<T>());
603 let result = tokio::select! {
604 () = shutdown.cancelled() => return,
605 result = attempt => result,
606 };
607
608 match result {
609 Ok(Ok(_client)) => {
610 info!(
611 "Configured service reachable",
612 dependency.name = dependency_name,
613 dependency.endpoint = endpoint.as_str()
614 );
615 shutdown.cancelled().await;
616 return;
617 },
618 Ok(Err(err)) if first_failure => {
619 warn!(
620 &err,
621 "Configured service unreachable",
622 dependency.name = dependency_name,
623 dependency.endpoint = endpoint.as_str()
624 );
625 },
626 Err(_elapsed) if first_failure => {
627 warn!(
628 "Configured service connection timed out",
629 dependency.name = dependency_name,
630 dependency.endpoint = endpoint.as_str(),
631 timeout.ms = CONNECT_TIMEOUT.as_millis() as u64
632 );
633 },
634 Ok(Err(err)) => {
635 debug!(
636 &err,
637 "Configured service still unreachable",
638 dependency.name = dependency_name,
639 dependency.endpoint = endpoint.as_str()
640 );
641 },
642 Err(_elapsed) => {
643 debug!(
644 "Configured service connection still timing out",
645 dependency.name = dependency_name,
646 dependency.endpoint = endpoint.as_str(),
647 timeout.ms = CONNECT_TIMEOUT.as_millis() as u64
648 );
649 },
650 }
651 first_failure = false;
652
653 let retry_delay = backoff.next().unwrap_or(RETRY_MAX);
654 tokio::select! {
655 () = shutdown.cancelled() => return,
656 () = tokio::time::sleep(retry_delay) => {},
657 }
658 }
659 }
660
661 fn connect_with_channel<T>(self, channel: Channel) -> T
662 where
663 T: GrpcClient,
664 {
665 let metadata_genesis = self.metadata_genesis.map(|genesis| genesis.to_hex());
666 let interceptor = Interceptor::new(
667 self.enable_otel,
668 self.metadata_version.as_deref(),
669 metadata_genesis.as_deref(),
670 self.metadata_auth_header_value,
671 );
672 T::with_interceptor(channel, interceptor)
673 }
674}
675
676impl ValidatorClient {
677 pub async fn submit_batch(
683 &mut self,
684 proposed_batch: &ProposedBatch,
685 sealed_transaction_inputs: &[SealedTransactionInputs],
686 ) -> Result<(), Status> {
687 if proposed_batch.transactions().len() != sealed_transaction_inputs.len() {
688 return Err(Status::invalid_argument(
689 "transaction inputs do not match the batch's transactions",
690 ));
691 }
692 for (tx, inputs) in proposed_batch.transactions().iter().zip(sealed_transaction_inputs) {
693 let proven_tx = GeneratedProvenTransaction {
694 transaction: Some(tx.as_ref().into()),
695 sealed_transaction_inputs: Some(inputs.clone()),
696 };
697 self.submit_proven_transaction(
698 crate::generated::miden::validator::v1::SubmitProvenTransactionRequest {
699 submission: Some(proven_tx),
700 },
701 )
702 .await?;
703 }
704 Ok(())
705 }
706}