miden_note_transport_node/node/grpc/
mod.rs1mod streaming;
2
3use std::collections::BTreeSet;
4use std::net::SocketAddr;
5use std::sync::Arc;
6use std::time::Duration;
7
8use chrono::Utc;
9use miden_note_transport_proto::miden_note_transport::miden_note_transport_server::MidenNoteTransportServer;
10use miden_note_transport_proto::miden_note_transport::{
11 FetchNotesRequest,
12 FetchNotesResponse,
13 SendNoteRequest,
14 SendNoteResponse,
15 StatsResponse,
16 StreamNotesRequest,
17 TransportNote,
18};
19use miden_protocol::utils::serde::Deserializable;
20use rand::Rng;
21use tokio::sync::mpsc;
22use tonic::Status;
23use tonic_web::GrpcWebLayer;
24use tower::limit::GlobalConcurrencyLimitLayer;
25use tower::timeout::TimeoutLayer;
26use tower_http::cors::{Any, CorsLayer};
27
28use self::streaming::{NoteStreamer, StreamerMessage, Sub, Subface};
29use crate::database::Database;
30use crate::metrics::MetricsGrpc;
31
32const MAX_TAGS_PER_FETCH_REQUEST: usize = 128;
43
44pub struct GrpcServer {
46 database: Arc<Database>,
47 config: GrpcServerConfig,
48 streamer: StreamerCtx,
49 metrics: MetricsGrpc,
50}
51
52#[derive(Clone, Debug)]
54pub struct GrpcServerConfig {
55 pub host: String,
57 pub port: u16,
59 pub max_note_size: usize,
61 pub max_connections: usize,
63 pub request_timeout: usize,
65}
66
67pub(super) struct StreamerCtx {
69 tx: mpsc::Sender<StreamerMessage>,
70 handle: tokio::task::JoinHandle<()>,
71}
72
73impl Default for GrpcServerConfig {
74 fn default() -> Self {
75 Self {
76 host: "127.0.0.1".to_string(),
77 port: 57292,
78 max_note_size: 512_000,
79 max_connections: 4096,
80 request_timeout: 4,
81 }
82 }
83}
84
85impl GrpcServer {
86 pub fn new(database: Arc<Database>, config: GrpcServerConfig, metrics: MetricsGrpc) -> Self {
88 let streamer = StreamerCtx::spawn(database.clone());
89 Self { database, config, streamer, metrics }
90 }
91
92 pub fn into_service(self) -> MidenNoteTransportServer<Self> {
94 MidenNoteTransportServer::new(self)
95 }
96
97 pub async fn serve(self) -> crate::Result<()> {
99 let (health_reporter, health_svc) = tonic_health::server::health_reporter();
100 health_reporter.set_serving::<MidenNoteTransportServer<Self>>().await;
101
102 let addr = format!("{}:{}", self.config.host, self.config.port)
103 .parse::<SocketAddr>()
104 .map_err(|e| crate::Error::Internal(format!("Invalid address: {e}")))?;
105
106 let cors = CorsLayer::new().allow_origin(Any).allow_headers(Any).allow_methods(Any);
107
108 tonic::transport::Server::builder()
109 .accept_http1(true)
110 .layer(cors)
111 .layer(GrpcWebLayer::new())
112 .layer(GlobalConcurrencyLimitLayer::new(self.config.max_connections))
113 .layer(TimeoutLayer::new(Duration::from_secs(self.config.request_timeout as u64)))
114 .add_service(health_svc)
115 .add_service(self.into_service())
116 .serve(addr)
117 .await
118 .map_err(|e| crate::Error::Internal(format!("Server error: {e}")))
119 }
120}
121
122impl StreamerCtx {
123 pub(super) fn spawn(database: Arc<Database>) -> Self {
127 let (tx, rx) = mpsc::channel(128);
128 let handle = tokio::spawn(NoteStreamer::new(database, rx).stream());
129 Self { tx, handle }
130 }
131}
132
133#[tonic::async_trait]
134impl miden_note_transport_proto::miden_note_transport::miden_note_transport_server::MidenNoteTransport
135 for GrpcServer
136{
137 #[tracing::instrument(skip(self, request), fields(
138 operation = "grpc.send_note.request",
139 note_size = tracing::field::Empty,
140 ))]
141 async fn send_note(
142 &self,
143 request: tonic::Request<SendNoteRequest>,
144 ) -> Result<tonic::Response<SendNoteResponse>, tonic::Status> {
145 let request_data = request.into_inner();
146 let pnote = request_data.note.ok_or_else(|| Status::invalid_argument("Missing note"))?;
147
148 let note_size = pnote.header.len() + pnote.details.len();
152 let span = tracing::Span::current();
153 span.record("note_size", note_size);
154
155 let timer = self.metrics.grpc_send_note_request(note_size as u64);
156
157 if note_size > self.config.max_note_size {
159 tracing::warn!(reason = "note_too_large", size = note_size, max = self.config.max_note_size, "send_note rejected");
160 return Err(Status::resource_exhausted(format!("Note too large ({note_size})")));
161 }
162
163 let header = miden_protocol::note::NoteHeader::read_from_bytes(&pnote.header)
165 .map_err(|e| {
166 tracing::warn!(reason = "invalid_header", "send_note rejected");
167 Status::invalid_argument(format!("Invalid header: {e:?}"))
168 })?;
169
170 tracing::debug!(
171 note_id = %header.id(),
172 tag = header.metadata().tag().as_u32(),
173 has_after_block_num = pnote.after_block_num.is_some(),
174 "send_note accepted"
175 );
176
177 let note_for_db = crate::types::StoredNote {
179 header,
180 details: pnote.details,
181 created_at: Utc::now(),
182 seq: 0,
184 after_block_num: pnote.after_block_num,
185 };
186
187 self.database
188 .store_note(¬e_for_db)
189 .await.map_err(|e| tonic::Status::internal(format!("Failed to store note: {e:?}")))?;
190
191 timer.finish("ok");
192
193 Ok(tonic::Response::new(SendNoteResponse {}))
194 }
195
196 #[tracing::instrument(skip(self, request), fields(
197 operation = "grpc.fetch_notes.request",
198 tag_count = tracing::field::Empty,
199 cursor = tracing::field::Empty,
200 notes_returned = tracing::field::Empty,
201 response_cursor = tracing::field::Empty,
202 ))]
203 async fn fetch_notes(
204 &self,
205 request: tonic::Request<FetchNotesRequest>,
206 ) -> Result<tonic::Response<FetchNotesResponse>, tonic::Status> {
207 let timer = self.metrics.grpc_fetch_notes_request();
208
209 let request_data = request.into_inner();
210
211 if request_data.tags.len() > MAX_TAGS_PER_FETCH_REQUEST {
217 tracing::warn!(
218 reason = "too_many_tags",
219 tag_count = request_data.tags.len(),
220 max = MAX_TAGS_PER_FETCH_REQUEST,
221 "fetch_notes rejected"
222 );
223 return Err(Status::invalid_argument(format!(
224 "Too many tags in fetch_notes request: {} (max {})",
225 request_data.tags.len(),
226 MAX_TAGS_PER_FETCH_REQUEST
227 )));
228 }
229
230 let tag_set: BTreeSet<_> = request_data.tags.into_iter().collect();
233 let tags: Vec<crate::types::NoteTag> = tag_set.into_iter().map(Into::into).collect();
234 let cursor = request_data.cursor;
235
236 let span = tracing::Span::current();
237 span.record("tag_count", tags.len());
238 span.record("cursor", cursor);
239
240 let stored_notes = self
246 .database
247 .fetch_notes_by_tags(&tags, cursor)
248 .await
249 .map_err(|e| tonic::Status::internal(format!("Failed to fetch notes: {e:?}")))?;
250
251 let mut rcursor = cursor;
252 for stored_note in &stored_notes {
253 let seq_cursor: u64 = stored_note
254 .seq
255 .try_into()
256 .map_err(|_| tonic::Status::internal("Negative seq in stored note"))?;
257 rcursor = rcursor.max(seq_cursor);
258 }
259
260 let proto_notes: Vec<_> = stored_notes.into_iter().map(TransportNote::from).collect();
261
262 span.record("notes_returned", proto_notes.len());
263 span.record("response_cursor", rcursor);
264
265 timer.finish("ok");
266
267 let proto_notes_size = proto_notes.iter().map(|pnote| (pnote.header.len() + pnote.details.len()) as u64).sum();
268 self.metrics.grpc_fetch_notes_response(
269 proto_notes.len() as u64,
270 proto_notes_size,
271 );
272
273 Ok(tonic::Response::new(FetchNotesResponse { notes: proto_notes, cursor: rcursor }))
274 }
275
276 type StreamNotesStream = Sub;
277 #[tracing::instrument(skip(self, request), fields(
278 operation = "grpc.stream_notes.request",
279 subscription_id = tracing::field::Empty,
280 ))]
281 async fn stream_notes(
282 &self,
283 request: tonic::Request<StreamNotesRequest>,
284 ) -> Result<tonic::Response<Self::StreamNotesStream>, tonic::Status> {
285 let request_data = request.into_inner();
286 let tag = request_data.tag.into();
287 let id: u64 = rand::rng().random();
288
289 let span = tracing::Span::current();
290 span.record("subscription_id", id);
291
292 tracing::debug!(tag = crate::types::NoteTag::as_u32(&tag), cursor = request_data.cursor, "stream_notes subscribe");
293
294 let (sub_tx, sub_rx) = mpsc::channel(32);
295 let sub = Sub::new(id, tag, sub_rx, self.streamer.tx.clone());
296 let subf = Subface::new(id, tag, sub_tx);
297 self.streamer.tx.try_send(StreamerMessage::AddSub(subf))
298 .map_err(|e| tonic::Status::internal(format!("Failed sending internal streamer message: {e}")))?;
299
300 Ok(tonic::Response::new(sub))
301 }
302
303 #[tracing::instrument(skip(self), fields(operation = "grpc.stats.request"))]
304 async fn stats(
305 &self,
306 _request: tonic::Request<()>,
307 ) -> Result<tonic::Response<StatsResponse>, tonic::Status> {
308 let (total_notes, total_tags) = self
309 .database
310 .get_stats()
311 .await.map_err(|e| tonic::Status::internal(format!("Failed to get stats: {e:?}")))?;
312
313 let response = StatsResponse {
314 total_notes,
315 total_tags,
316 notes_per_tag: Vec::new(), };
318
319 Ok(tonic::Response::new(response))
320 }
321}
322
323impl Drop for StreamerCtx {
324 fn drop(&mut self) {
325 if let Err(e) = self.tx.try_send(StreamerMessage::Shutdown) {
326 tracing::error!("Streamer shutdown message sending failure: {e}");
327 self.handle.abort();
328 }
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use std::sync::Arc;
335
336 use miden_note_transport_proto::miden_note_transport::FetchNotesRequest;
337 use miden_note_transport_proto::miden_note_transport::miden_note_transport_server::MidenNoteTransport;
338
339 use super::*;
340 use crate::database::{Database, DatabaseConfig};
341 use crate::metrics::Metrics;
342
343 async fn test_server() -> GrpcServer {
344 let metrics = Metrics::default();
345 let db = Arc::new(
346 Database::connect(DatabaseConfig::default(), metrics.db.clone()).await.unwrap(),
347 );
348 GrpcServer::new(db, GrpcServerConfig::default(), metrics.grpc)
349 }
350
351 #[tokio::test]
356 async fn test_fetch_notes_rejects_too_many_tags() {
357 let server = test_server().await;
358
359 let tags = vec![0u32; MAX_TAGS_PER_FETCH_REQUEST + 1];
360 let request = tonic::Request::new(FetchNotesRequest { tags, cursor: 0 });
361 let result = server.fetch_notes(request).await;
362
363 let status = result.expect_err("expected InvalidArgument");
364 assert_eq!(status.code(), tonic::Code::InvalidArgument);
365 assert!(
366 status.message().contains("Too many tags"),
367 "unexpected error message: {}",
368 status.message()
369 );
370 }
371
372 #[tokio::test]
376 async fn test_fetch_notes_accepts_max_tags_at_limit() {
377 let server = test_server().await;
378
379 let tags = vec![0u32; MAX_TAGS_PER_FETCH_REQUEST];
380 let request = tonic::Request::new(FetchNotesRequest { tags, cursor: 0 });
381 let result = server.fetch_notes(request).await;
382
383 let response = result.expect("request at the cap must succeed").into_inner();
384 assert_eq!(response.notes.len(), 0, "DB is empty, no notes returned");
385 assert_eq!(response.cursor, 0);
386 }
387}