1use core::marker::PhantomData;
2use core::mem::MaybeUninit;
3
4use crate::engine::EngineState;
5use crate::engine::RatchetPolicy;
6use crate::identity::held::HoldIdentityError;
7use crate::identity::{Zeroizing, IDENTITY_SECRET_KEY_LEN};
8use crate::routing::links::resources::ResourceStrategy;
9use crate::routing::request_handlers::RequestHandlerError;
10use crate::routing::upstream_app_destinations::RegisterDestinationError;
11use crate::routing::{LinkRequestPolicy, ProofStrategy};
12use crate::storage::StorageLayout;
13use crate::storage::TablePushError;
14use crate::units::ByteLimit;
15use crate::wire::DestinationHash;
16
17use super::super::request_endpoints::RequestEndpointSet;
18use super::super::PrnsEvent;
19use super::recipe::{PreConfiguredDestination, PrnsNodeRecipe, ServeMyRequestEndpoints};
20
21pub struct AssembledNode<St, R, F, S>
22where
23 S: StorageLayout,
24{
25 pub engine: EngineState<S>,
26 pub state: St,
27 pub on_event: F,
28 pub request_endpoints: PhantomData<R>,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ConfigurePreconfiguredDestinationError {
33 HoldIdentity(HoldIdentityError),
34 Register(RegisterDestinationError),
35 RegisterRequestHandler(TablePushError),
36 SeedRequester(RequestHandlerError),
37 ServesEmptyEndpointSet,
38}
39
40struct SingleDestinationConfiguration<'a> {
41 app_name: &'a str,
42 aspects: &'a [&'a str],
43 identity: Zeroizing<[u8; IDENTITY_SECRET_KEY_LEN]>,
44 app_data: &'a [u8],
45 proof: ProofStrategy,
46 link_requests: LinkRequestPolicy,
47 ratchet: RatchetPolicy,
48 resource_strategy: ResourceStrategy,
49 maximum_request_bytes: ByteLimit,
50}
51
52pub fn configure_preconfigured_destination<'a, St, R, S>(
53 engine: &mut EngineState<S>,
54 destination: PreConfiguredDestination<'a>,
55) -> Result<DestinationHash, ConfigurePreconfiguredDestinationError>
56where
57 R: RequestEndpointSet<St>,
58 S: StorageLayout,
59{
60 match destination {
61 PreConfiguredDestination::Plain { app_name, aspects } => engine
62 .register_plain_destination(app_name, aspects)
63 .map_err(ConfigurePreconfiguredDestinationError::Register),
64 PreConfiguredDestination::Single {
65 app_name,
66 aspects,
67 identity,
68 announce_app_data,
69 proof,
70 link_requests,
71 ratchet,
72 resource_strategy,
73 maximum_request_bytes,
74 request_endpoints,
75 } => configure_single_destination::<St, R, S>(
76 engine,
77 SingleDestinationConfiguration {
78 app_name,
79 aspects,
80 identity,
81 app_data: announce_app_data,
82 proof,
83 link_requests,
84 ratchet,
85 resource_strategy,
86 maximum_request_bytes,
87 },
88 request_endpoints,
89 ),
90 }
91}
92
93fn configure_single_destination<St, R, S>(
94 engine: &mut EngineState<S>,
95 configuration: SingleDestinationConfiguration<'_>,
96 request_endpoints: ServeMyRequestEndpoints,
97) -> Result<DestinationHash, ConfigurePreconfiguredDestinationError>
98where
99 R: RequestEndpointSet<St>,
100 S: StorageLayout,
101{
102 let SingleDestinationConfiguration {
103 app_name,
104 aspects,
105 identity,
106 app_data,
107 proof,
108 link_requests,
109 ratchet,
110 resource_strategy,
111 maximum_request_bytes,
112 } = configuration;
113 let held = engine
114 .hold_identity(identity)
115 .map_err(ConfigurePreconfiguredDestinationError::HoldIdentity)?;
116 let destination = engine
117 .register_single_destination(
118 &held,
119 app_name,
120 aspects,
121 app_data,
122 proof,
123 link_requests,
124 ratchet,
125 )
126 .map_err(ConfigurePreconfiguredDestinationError::Register)?;
127 engine.set_default_resource_strategy(&destination, resource_strategy);
128 engine.set_maximum_request_bytes(&destination, maximum_request_bytes);
129 if matches!(request_endpoints, ServeMyRequestEndpoints::Yes) {
130 register_request_routes_for::<St, R, S>(engine, destination)?;
131 }
132 Ok(destination)
133}
134
135fn register_request_routes_for<St, R, S>(
136 engine: &mut EngineState<S>,
137 destination: DestinationHash,
138) -> Result<(), ConfigurePreconfiguredDestinationError>
139where
140 R: RequestEndpointSet<St>,
141 S: StorageLayout,
142{
143 for (path, policy) in R::REGISTRATIONS {
144 engine
145 .register_request_handler(&destination, path, policy.engine_policy())
146 .map_err(ConfigurePreconfiguredDestinationError::RegisterRequestHandler)?;
147 for seed in policy.seed_list() {
148 engine
149 .allow_requester(&destination, path, *seed)
150 .map_err(ConfigurePreconfiguredDestinationError::SeedRequester)?;
151 }
152 }
153 Ok(())
154}
155
156#[allow(clippy::expect_used)]
157pub fn assemble_node<'a, D, St, R, F, I, S, P>(
158 recipe: PrnsNodeRecipe<D, St, R, F, I, S, P>,
159) -> (AssembledNode<St, R, F, S>, I, P)
160where
161 D: IntoIterator<Item = PreConfiguredDestination<'a>>,
162 R: RequestEndpointSet<St>,
163 F: FnMut(PrnsEvent<'_>, &St),
164 S: StorageLayout,
165{
166 let PrnsNodeRecipe {
167 transport_identity,
168 pre_configured_destinations,
169 app_state,
170 storage: _,
171 request_endpoints: _,
172 interfaces,
173 persistence,
174 on_event,
175 } = recipe;
176
177 let mut node = AssembledNode {
178 engine: EngineState::<S>::default(),
179 state: app_state,
180 on_event,
181 request_endpoints: PhantomData,
182 };
183 configure_assembled_node(&mut node, pre_configured_destinations, transport_identity);
184 (node, interfaces, persistence)
185}
186
187#[expect(
188 unsafe_code,
189 clippy::undocumented_unsafe_blocks,
190 reason = "every AssembledNode field is initialized before the slot is exposed"
191)]
192pub fn assemble_node_in_place<'a, 'slot, D, St, R, F, I, S, P>(
193 slot: &'slot mut MaybeUninit<AssembledNode<St, R, F, S>>,
194 recipe: PrnsNodeRecipe<D, St, R, F, I, S, P>,
195) -> (&'slot mut AssembledNode<St, R, F, S>, I, P)
196where
197 D: IntoIterator<Item = PreConfiguredDestination<'a>>,
198 R: RequestEndpointSet<St>,
199 F: FnMut(PrnsEvent<'_>, &St),
200 S: StorageLayout,
201{
202 let PrnsNodeRecipe {
203 transport_identity,
204 pre_configured_destinations,
205 app_state,
206 storage: _,
207 request_endpoints: _,
208 interfaces,
209 persistence,
210 on_event,
211 } = recipe;
212 let node = slot.as_mut_ptr();
213 unsafe {
214 let engine =
215 &mut *core::ptr::addr_of_mut!((*node).engine).cast::<MaybeUninit<EngineState<S>>>();
216 EngineState::init_in_place(engine);
217 core::ptr::addr_of_mut!((*node).state).write(app_state);
218 core::ptr::addr_of_mut!((*node).on_event).write(on_event);
219 core::ptr::addr_of_mut!((*node).request_endpoints).write(PhantomData);
220 }
221 let node = unsafe { slot.assume_init_mut() };
222 configure_assembled_node(node, pre_configured_destinations, transport_identity);
223 (node, interfaces, persistence)
224}
225
226#[allow(clippy::expect_used)]
227fn configure_assembled_node<'a, D, St, R, F, S>(
228 node: &mut AssembledNode<St, R, F, S>,
229 pre_configured_destinations: D,
230 transport_identity: Option<Zeroizing<[u8; IDENTITY_SECRET_KEY_LEN]>>,
231) where
232 D: IntoIterator<Item = PreConfiguredDestination<'a>>,
233 R: RequestEndpointSet<St>,
234 F: FnMut(PrnsEvent<'_>, &St),
235 S: StorageLayout,
236{
237 let mut any_destination_declared = false;
238 let mut any_destination_serves = false;
239 for destination in pre_configured_destinations {
240 any_destination_declared = true;
241 any_destination_serves |= matches!(
242 destination,
243 PreConfiguredDestination::Single {
244 request_endpoints: ServeMyRequestEndpoints::Yes,
245 ..
246 }
247 );
248 configure_preconfigured_destination::<St, R, S>(&mut node.engine, destination)
249 .expect("recipe destination is valid and fits the store");
250 }
251 assert!(
252 R::REGISTRATIONS.is_empty() || any_destination_serves || !any_destination_declared,
253 "the recipe declares request endpoints but no destination serves them; set request_endpoints: ServeMyRequestEndpoints::Yes on a destination"
254 );
255
256 if let Some(secret) = transport_identity {
257 let identity = node
258 .engine
259 .hold_identity(secret)
260 .expect("the transport identity fits the held-identity store");
261 node.engine
262 .set_transport_identity(&identity)
263 .expect("the transport identity was just held");
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use crate::identity::IdentityHash;
271 use crate::routing::request_handlers::RequestPathHash;
272 use crate::runtime::request_endpoints::{Decline, RequestContext, RequestEndpointPolicy};
273 use crate::runtime::{ManuallyAttached, NoPersistence};
274 use crate::storage::TestFixedStorage;
275
276 type Storage = TestFixedStorage<4, 4, 128, 4, 4, 4, 2, 2, 2, 2, 2, 2>;
277
278 struct Routes;
279
280 impl RequestEndpointSet<()> for Routes {
281 const REGISTRATIONS: &'static [(&'static str, RequestEndpointPolicy)] =
282 &[("/test", RequestEndpointPolicy::AllowList(&[]))];
283
284 async fn dispatch(
285 _cx: RequestContext<'_, ()>,
286 _path_hash: RequestPathHash,
287 ) -> Result<(), Decline> {
288 Err(Decline::Ignore)
289 }
290 }
291
292 fn configured_engine(
293 request_endpoints: ServeMyRequestEndpoints,
294 maximum_request_bytes: ByteLimit,
295 ) -> (EngineState<Storage>, DestinationHash) {
296 let mut engine = EngineState::<Storage>::default();
297 let destination = configure_preconfigured_destination::<(), Routes, Storage>(
298 &mut engine,
299 PreConfiguredDestination::Single {
300 app_name: "test",
301 aspects: &["requests"],
302 identity: Zeroizing::new([0x11; IDENTITY_SECRET_KEY_LEN]),
303 announce_app_data: &[],
304 proof: ProofStrategy::ProveAll,
305 link_requests: LinkRequestPolicy::AcceptAll,
306 ratchet: RatchetPolicy::NoRatchets,
307 resource_strategy: ResourceStrategy::AcceptNone,
308 maximum_request_bytes,
309 request_endpoints,
310 },
311 )
312 .expect("the test destination fits fixed storage");
313 (engine, destination)
314 }
315
316 #[test]
317 fn node_route_set_attaches_routes_to_the_destination() {
318 let (mut engine, destination) =
319 configured_engine(ServeMyRequestEndpoints::Yes, ByteLimit::Unlimited);
320
321 assert_eq!(
322 engine.allow_requester(&destination, "/test", IdentityHash::new([0x22; 16])),
323 Ok(())
324 );
325 }
326
327 #[test]
328 fn none_leaves_routes_unattached_from_the_destination() {
329 let (mut engine, destination) =
330 configured_engine(ServeMyRequestEndpoints::No, ByteLimit::Unlimited);
331
332 assert_eq!(
333 engine.allow_requester(&destination, "/test", IdentityHash::new([0x22; 16])),
334 Err(RequestHandlerError::NoSuchHandler)
335 );
336 }
337
338 #[test]
339 fn recipe_request_limit_reaches_the_registered_destination() {
340 let (engine, destination) =
341 configured_engine(ServeMyRequestEndpoints::No, ByteLimit::Maximum(1_024));
342
343 assert_eq!(
344 engine
345 .upstream_app_destinations()
346 .find(|registered| registered.destination == destination)
347 .and_then(|registered| match registered.kind {
348 crate::routing::upstream_app_destinations::UpstreamAppDestinationKind::Single {
349 maximum_request_bytes,
350 ..
351 } => Some(maximum_request_bytes),
352 crate::routing::upstream_app_destinations::UpstreamAppDestinationKind::Plain
353 | crate::routing::upstream_app_destinations::UpstreamAppDestinationKind::Group => None,
354 }),
355 Some(ByteLimit::Maximum(1_024)),
356 );
357 }
358
359 #[test]
360 fn in_place_assembly_initializes_and_configures_the_node() {
361 let mut slot = MaybeUninit::uninit();
362 let storage: Storage = TestFixedStorage;
363 let (node, ManuallyAttached, NoPersistence) = assemble_node_in_place(
364 &mut slot,
365 PrnsNodeRecipe {
366 transport_identity: Some(Zeroizing::new([0x33; IDENTITY_SECRET_KEY_LEN])),
367 pre_configured_destinations: [PreConfiguredDestination::Plain {
368 app_name: "test",
369 aspects: &["plain"],
370 }],
371 app_state: (),
372 storage,
373 request_endpoints: (),
374 interfaces: ManuallyAttached,
375 persistence: NoPersistence,
376 on_event: |_, _| {},
377 },
378 );
379
380 assert!(node.engine.network_transport_enabled());
381 assert_eq!(node.engine.held_identity_hashes().len(), 1);
382 assert_eq!(node.engine.upstream_app_destinations().count(), 1);
383 }
384
385 #[test]
386 #[should_panic(expected = "no destination serves them")]
387 fn declared_endpoints_with_no_serving_destination_fail_loudly() {
388 let mut slot = MaybeUninit::uninit();
389 let storage: Storage = TestFixedStorage;
390 let (_node, ManuallyAttached, NoPersistence) = assemble_node_in_place(
391 &mut slot,
392 PrnsNodeRecipe {
393 transport_identity: None,
394 pre_configured_destinations: [PreConfiguredDestination::Plain {
395 app_name: "test",
396 aspects: &["plain"],
397 }],
398 app_state: (),
399 storage,
400 request_endpoints: Routes,
401 interfaces: ManuallyAttached,
402 persistence: NoPersistence,
403 on_event: |_, _| {},
404 },
405 );
406 }
407}