1mod client;
5mod conversions;
6mod node_provider;
7pub mod pool;
9mod relay;
10#[cfg(with_server)]
11mod server;
12pub mod transport;
14
15pub use client::*;
16pub use conversions::*;
17pub use node_provider::*;
18pub use relay::{RelayClient, RelayNodeProvider};
19#[cfg(with_server)]
20pub use server::*;
21
22pub mod api {
24 #![allow(missing_docs)]
26 tonic::include_proto!("rpc.v1");
27}
28
29#[derive(thiserror::Error, Debug)]
30#[allow(missing_docs)]
31pub enum GrpcError {
32 #[error("failed to connect to address: {0}")]
33 ConnectionFailed(#[from] transport::Error),
34
35 #[error("failed to execute task to completion: {0}")]
36 Join(#[from] futures::channel::oneshot::Canceled),
37
38 #[error("failed to parse socket address: {0}")]
39 SocketAddr(#[from] std::net::AddrParseError),
40
41 #[cfg(with_server)]
42 #[error(transparent)]
43 Reflection(#[from] tonic_reflection::server::Error),
44}
45
46const MEBIBYTE: usize = 1024 * 1024;
47pub const GRPC_MAX_MESSAGE_SIZE: usize = 16 * MEBIBYTE;
49
50pub const GRPC_CHUNKED_MESSAGE_FILL_LIMIT: usize = GRPC_MAX_MESSAGE_SIZE * 7 / 10;
53
54pub const METHOD_NAME_LABEL: &str = "method_name";
56
57pub const TRAFFIC_TYPE_LABEL: &str = "traffic_type";
59
60pub const ERROR_TYPE_LABEL: &str = "error_type";
62
63const MAX_PROTO_IDENT_LEN: usize = 128;
67
68fn is_proto_identifier(s: &str) -> bool {
71 if s.is_empty() || s.len() > MAX_PROTO_IDENT_LEN {
72 return false;
73 }
74 let mut bytes = s.bytes();
75 let first = bytes.next().expect("non-empty checked above");
76 first.is_ascii_alphabetic() && bytes.all(|b| b.is_ascii_alphanumeric() || b == b'_')
77}
78
79fn is_proto_service_name(s: &str) -> bool {
82 let mut parts = s.split('.');
83 let Some(first) = parts.next() else {
84 return false;
85 };
86 let Some(second) = parts.next() else {
87 return false;
88 };
89 is_proto_identifier(first) && is_proto_identifier(second) && parts.all(is_proto_identifier)
90}
91
92pub fn extract_grpc_method_name(path: &str) -> &str {
101 let mut parts = path.splitn(3, '/');
102 let (Some(""), Some(service), Some(method)) = (parts.next(), parts.next(), parts.next()) else {
103 return "non_grpc";
104 };
105 if is_proto_service_name(service) && is_proto_identifier(method) {
106 method
107 } else {
108 "non_grpc"
109 }
110}
111
112#[cfg(with_metrics)]
113pub(crate) fn init_metrics() {
114 client::metrics::init_metrics();
115 #[cfg(with_server)]
116 server::metrics::init_metrics();
117}
118
119#[cfg(test)]
120mod method_name_tests {
121 use super::*;
122
123 #[test]
124 fn grpc_unary_method() {
125 assert_eq!(
126 extract_grpc_method_name("/rpc.v1.ValidatorNode/HandleBlockProposal"),
127 "HandleBlockProposal"
128 );
129 }
130
131 #[test]
132 fn grpc_streaming_method() {
133 assert_eq!(
134 extract_grpc_method_name("/rpc.v1.ValidatorNode/SubscribeToNotifications"),
135 "SubscribeToNotifications"
136 );
137 }
138
139 #[test]
140 fn health_check_path() {
141 assert_eq!(
142 extract_grpc_method_name("/grpc.health.v1.Health/Check"),
143 "Check"
144 );
145 }
146
147 #[test]
148 fn non_grpc_root_path() {
149 assert_eq!(extract_grpc_method_name("/"), "non_grpc");
150 }
151
152 #[test]
153 fn non_grpc_plain_path() {
154 assert_eq!(extract_grpc_method_name("/healthz"), "non_grpc");
155 }
156
157 #[test]
158 fn non_grpc_no_dot_in_service() {
159 assert_eq!(extract_grpc_method_name("/NoDotService/Method"), "non_grpc");
160 }
161
162 #[test]
163 fn empty_path() {
164 assert_eq!(extract_grpc_method_name(""), "non_grpc");
165 }
166
167 #[test]
168 fn dot_env_bot_scan_does_not_leak_into_label() {
169 assert_eq!(
170 extract_grpc_method_name(
171 "/.env.local/.env.production/.env.staging/.env.development/.env.test"
172 ),
173 "non_grpc"
174 );
175 }
176
177 #[test]
178 fn service_starting_with_dot_is_rejected() {
179 assert_eq!(extract_grpc_method_name("/.foo.bar/Method"), "non_grpc");
180 }
181
182 #[test]
183 fn method_with_extra_path_segments_is_rejected() {
184 assert_eq!(
185 extract_grpc_method_name("/foo.bar/Method/extra"),
186 "non_grpc"
187 );
188 }
189
190 #[test]
191 fn method_with_invalid_characters_is_rejected() {
192 assert_eq!(extract_grpc_method_name("/foo.bar/Method-x"), "non_grpc");
193 assert_eq!(extract_grpc_method_name("/foo.bar/Method?x"), "non_grpc");
194 assert_eq!(extract_grpc_method_name("/foo.bar/.Method"), "non_grpc");
195 }
196
197 #[test]
198 fn identifier_starting_with_underscore_is_rejected() {
199 assert_eq!(extract_grpc_method_name("/foo.bar/_Method"), "non_grpc");
200 assert_eq!(extract_grpc_method_name("/_foo.bar/Method"), "non_grpc");
201 assert_eq!(
202 extract_grpc_method_name("/foo.bar/________________"),
203 "non_grpc"
204 );
205 }
206
207 #[test]
208 fn empty_method_segment_is_rejected() {
209 assert_eq!(extract_grpc_method_name("/foo.bar/"), "non_grpc");
210 }
211
212 #[test]
213 fn empty_service_segment_is_rejected() {
214 assert_eq!(extract_grpc_method_name("//Method"), "non_grpc");
215 }
216
217 #[test]
218 fn service_with_consecutive_dots_is_rejected() {
219 assert_eq!(extract_grpc_method_name("/foo..bar/Method"), "non_grpc");
220 }
221
222 #[test]
223 fn overlong_method_is_rejected() {
224 let long_method = "M".repeat(MAX_PROTO_IDENT_LEN + 1);
225 let path = format!("/foo.bar/{long_method}");
226 assert_eq!(extract_grpc_method_name(&path), "non_grpc");
227 }
228
229 #[test]
230 fn path_without_leading_slash_is_rejected() {
231 assert_eq!(extract_grpc_method_name("foo.bar/Method"), "non_grpc");
232 }
233}