Skip to main content

prns_runtime/runtime/node/
assembly.rs

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}