Skip to main content

blut_graph_core/
wire.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3use alloc::collections::{BTreeMap, BTreeSet};
4use alloc::vec::Vec;
5use core::fmt;
6
7use crate::compile::hash_plan;
8use crate::model::{CompiledPlan, Effect, ExecutionRealm, InputBinding, OutputBinding, PlanId};
9
10const MAGIC: &[u8; 4] = b"BGP3";
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub struct PlanLimits {
14    pub max_bytes: usize,
15    pub max_nodes: usize,
16    pub max_buffers: usize,
17    pub max_contract_entries: usize,
18    pub max_peak_bytes: u64,
19    pub max_persistent_state_bytes: u64,
20    pub max_feedback_edges: usize,
21    pub max_subgraph_depth: usize,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub struct PlanAuthorization {
26    pub expected_realm: ExecutionRealm,
27    pub expected_plan_id: PlanId,
28}
29
30impl Default for PlanLimits {
31    fn default() -> Self {
32        Self {
33            max_bytes: 1024 * 1024,
34            max_nodes: 65_536,
35            max_buffers: 262_144,
36            max_contract_entries: 65_536,
37            max_peak_bytes: 64 * 1024 * 1024,
38            max_persistent_state_bytes: 64 * 1024 * 1024,
39            max_feedback_edges: 65_536,
40            max_subgraph_depth: 16,
41        }
42    }
43}
44
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub enum PlanDecodeError {
47    TooLarge,
48    BadMagic,
49    Malformed,
50    UnsupportedSchema(u32),
51    LimitExceeded,
52    ResourceLimitExceeded,
53    IdentityMismatch,
54    InvalidBuffer,
55    InvalidPlan,
56    RealmMismatch,
57    /// A vocabulary token this version assigns no meaning to. Before 0.3.0 the
58    /// derived enum bound produced `Malformed` for the same input.
59    UnknownToken,
60    UnauthorizedPlan,
61}
62
63impl fmt::Display for PlanDecodeError {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "{self:?}")
66    }
67}
68
69#[cfg(feature = "std")]
70impl std::error::Error for PlanDecodeError {}
71
72fn valid_compiled_port(port: &crate::CompiledPortContract) -> bool {
73    let descriptor = crate::PortDescriptor {
74        name: port.name.clone(),
75        semantic_type: port.semantic_type.clone(),
76        optional: port.optional,
77        layouts: alloc::vec![port.layout],
78        max_bytes: port.max_bytes,
79        domain: port.domain.clone(),
80        proof: port.proof.clone(),
81        policy: port.policy.clone(),
82        fidelity: port.fidelity.clone(),
83        extent: port.extent.clone(),
84        lease: port.lease.clone(),
85    };
86    crate::compile::valid_port_contract(&descriptor)
87        && port.proof.requires.windows(2).all(|pair| pair[0] < pair[1])
88        && port.proof.provides.windows(2).all(|pair| pair[0] < pair[1])
89        && port
90            .proof
91            .invalidates
92            .windows(2)
93            .all(|pair| pair[0] < pair[1])
94        && port
95            .policy
96            .requires
97            .windows(2)
98            .all(|pair| pair[0] < pair[1])
99        && port.policy.adds.windows(2).all(|pair| pair[0] < pair[1])
100}
101
102fn contract_entry_count(port: &crate::CompiledPortContract) -> Option<usize> {
103    0usize
104        .checked_add(port.proof.requires.len())?
105        .checked_add(port.proof.provides.len())?
106        .checked_add(port.proof.invalidates.len())?
107        .checked_add(port.policy.requires.len())?
108        .checked_add(port.policy.adds.len())?
109        .checked_add(port.extent.maximum_shape.len())
110}
111
112impl CompiledPlan {
113    /// Deterministic AOT bytes. Ordered collections are fixed by compilation;
114    /// postcard encodes the schema without host layout or pointer dependence.
115    pub fn to_aot_bytes(&self) -> Result<Vec<u8>, PlanDecodeError> {
116        let mut bytes = Vec::from(MAGIC.as_slice());
117        bytes.extend(postcard::to_allocvec(self).map_err(|_| PlanDecodeError::Malformed)?);
118        Ok(bytes)
119    }
120
121    /// Decode untrusted AOT bytes under structural limits and re-derive the
122    /// physical plan identity before returning an executable plan. `max_bytes`
123    /// is the allocation bound during postcard decode; the count limits are
124    /// post-decode semantic bounds within that already-bounded envelope.
125    pub fn from_aot_bytes(bytes: &[u8], limits: PlanLimits) -> Result<Self, PlanDecodeError> {
126        if bytes.len() > limits.max_bytes {
127            return Err(PlanDecodeError::TooLarge);
128        }
129        let body = bytes.strip_prefix(MAGIC).ok_or(PlanDecodeError::BadMagic)?;
130        let (plan, remainder): (Self, &[u8]) =
131            postcard::take_from_bytes(body).map_err(|_| PlanDecodeError::Malformed)?;
132        if !remainder.is_empty() {
133            return Err(PlanDecodeError::Malformed);
134        }
135        if plan.schema_version != 3 {
136            return Err(PlanDecodeError::UnsupportedSchema(plan.schema_version));
137        }
138        // The vocabulary tokens became transparent `u32` newtypes in 0.3.0, and
139        // that quietly removed a check nobody had written down: the derived
140        // `Deserialize` of a fieldless enum rejects an out-of-range variant
141        // index, so `realm = 7` could never survive a decode. A newtype accepts
142        // any `u32`, so the check is explicit here. Untrusted bytes reach this
143        // function by construction — that is what it is for.
144        if !plan.realm.is_known() {
145            return Err(PlanDecodeError::UnknownToken);
146        }
147        if plan.buffers.iter().any(|buffer| !buffer.layout.is_known()) {
148            return Err(PlanDecodeError::UnknownToken);
149        }
150        if plan.nodes.iter().any(|node| {
151            node.input_contracts
152                .iter()
153                .chain(node.output_contracts.iter())
154                .any(|contract| !contract.layout.is_known())
155                || node.conversion.as_ref().is_some_and(|conversion| {
156                    !conversion.from.is_known() || !conversion.to.is_known()
157                })
158        }) {
159            return Err(PlanDecodeError::UnknownToken);
160        }
161        if plan.nodes.len() > limits.max_nodes
162            || plan.order.len() > limits.max_nodes
163            || plan.buffers.len() > limits.max_buffers
164            || plan.invocation_ports.len() > limits.max_contract_entries
165            || plan.propagated_proofs.len() > limits.max_contract_entries
166            || plan.propagated_policy.len() > limits.max_contract_entries
167            || plan.feedback.len() > limits.max_feedback_edges
168        {
169            return Err(PlanDecodeError::LimitExceeded);
170        }
171        if plan
172            .invocation_ports
173            .windows(2)
174            .any(|pair| pair[0] >= pair[1])
175            || plan
176                .propagated_proofs
177                .windows(2)
178                .any(|pair| pair[0] >= pair[1])
179            || plan
180                .propagated_policy
181                .windows(2)
182                .any(|pair| pair[0] >= pair[1])
183            || plan.propagated_proofs.iter().any(|entry| entry.is_empty())
184            || plan.propagated_policy.iter().any(|entry| entry.is_empty())
185        {
186            return Err(PlanDecodeError::InvalidPlan);
187        }
188        if plan.peak_bytes > limits.max_peak_bytes
189            || plan.persistent_state_bytes > limits.max_persistent_state_bytes
190        {
191            return Err(PlanDecodeError::ResourceLimitExceeded);
192        }
193        if plan.order.is_empty() || plan.nodes.is_empty() {
194            return Err(PlanDecodeError::InvalidPlan);
195        }
196        let semantic_positions: BTreeMap<_, _> = plan
197            .order
198            .iter()
199            .enumerate()
200            .map(|(index, node)| (*node, index))
201            .collect();
202        if semantic_positions.len() != plan.order.len()
203            || plan
204                .nodes
205                .iter()
206                .flat_map(|node| node.semantic_nodes.iter())
207                .copied()
208                .ne(plan.order.iter().copied())
209        {
210            return Err(PlanDecodeError::InvalidPlan);
211        }
212        let mut buffer_bytes = 0u64;
213        for (index, buffer) in plan.buffers.iter().enumerate() {
214            // Compiler output uses dense, ID-ordered buffers so executor lookup
215            // remains O(1); hand-built sparse plans are not valid AOT inputs.
216            if buffer.id.0 as usize != index
217                || buffer.capacity_bytes == 0
218                || buffer.consumers.is_empty()
219                || !buffer.consumers.contains(&buffer.last_consumer)
220                || buffer.producer.0 as usize >= plan.nodes.len()
221                || buffer
222                    .consumers
223                    .iter()
224                    .any(|consumer| consumer.0 as usize >= plan.nodes.len())
225                || buffer.consumers.iter().collect::<BTreeSet<_>>().len() != buffer.consumers.len()
226                || buffer.consumers.windows(2).any(|pair| pair[0] >= pair[1])
227                || buffer.consumers.iter().max() != Some(&buffer.last_consumer)
228                || buffer
229                    .consumers
230                    .iter()
231                    .any(|consumer| *consumer <= buffer.producer)
232                || !plan.nodes[buffer.producer.0 as usize]
233                    .output_bindings
234                    .contains(&OutputBinding::Buffer(buffer.id))
235                || buffer.consumers.iter().any(|consumer| {
236                    !plan.nodes[consumer.0 as usize]
237                        .input_bindings
238                        .contains(&InputBinding::Buffer(buffer.id))
239                })
240                || buffer.aliases.is_some_and(|alias| {
241                    alias.0 >= buffer.id.0
242                        || plan.buffers[alias.0 as usize].aliases.is_some()
243                        || plan.buffers[alias.0 as usize].layout != buffer.layout
244                        || plan.buffers[alias.0 as usize].capacity_bytes < buffer.capacity_bytes
245                })
246            {
247                return Err(PlanDecodeError::InvalidBuffer);
248            }
249            if buffer.aliases.is_none() {
250                buffer_bytes = buffer_bytes
251                    .checked_add(buffer.capacity_bytes)
252                    .ok_or(PlanDecodeError::ResourceLimitExceeded)?;
253            }
254        }
255        let mut alias_intervals: BTreeMap<_, Vec<_>> = BTreeMap::new();
256        for buffer in &plan.buffers {
257            let root = buffer.aliases.unwrap_or(buffer.id);
258            alias_intervals
259                .entry(root)
260                .or_default()
261                .push((buffer.producer, buffer.last_consumer));
262        }
263        for intervals in alias_intervals.values_mut() {
264            intervals.sort_unstable();
265            if intervals.windows(2).any(|pair| pair[0].1 >= pair[1].0) {
266                return Err(PlanDecodeError::InvalidBuffer);
267            }
268        }
269        let mut workspace_bytes = 0u64;
270        let mut failure_entries = 0usize;
271        let mut used_invocations = BTreeSet::new();
272        let mut used_feedback = BTreeSet::new();
273        let mut state_bytes = 0u64;
274        for (index, node) in plan.nodes.iter().enumerate() {
275            failure_entries = failure_entries
276                .checked_add(node.failure.domains.len())
277                .ok_or(PlanDecodeError::LimitExceeded)?;
278            failure_entries = node
279                .semantic_configs
280                .iter()
281                .try_fold(failure_entries, |count, config| {
282                    count.checked_add(config.len())
283                })
284                .ok_or(PlanDecodeError::LimitExceeded)?;
285            failure_entries = node
286                .input_contracts
287                .iter()
288                .chain(&node.output_contracts)
289                .try_fold(failure_entries, |count, port| {
290                    count.checked_add(contract_entry_count(port)?)
291                })
292                .and_then(|count| count.checked_add(node.subgraph_path.len()))
293                .ok_or(PlanDecodeError::LimitExceeded)?;
294            let conversion = node.conversion.is_some();
295            for invocation in node
296                .input_bindings
297                .iter()
298                .filter_map(|binding| match binding {
299                    InputBinding::Invocation(invocation) => Some(*invocation),
300                    _ => None,
301                })
302            {
303                if !used_invocations.insert(invocation) {
304                    return Err(PlanDecodeError::InvalidPlan);
305                }
306            }
307            for feedback in node
308                .input_bindings
309                .iter()
310                .filter_map(|binding| match binding {
311                    InputBinding::Feedback(feedback) => Some(*feedback),
312                    _ => None,
313                })
314            {
315                if !used_feedback.insert(feedback) {
316                    return Err(PlanDecodeError::InvalidPlan);
317                }
318            }
319            if node.id.0 as usize != index
320                || conversion != node.semantic_nodes.is_empty()
321                || node.semantic_nodes.len() != node.semantic_types.len()
322                || node.semantic_nodes.len() != node.semantic_configs.len()
323                || node.input_ports.len() != node.input_bindings.len()
324                || node.output_ports.len() != node.output_bindings.len()
325                || node.input_contracts.len() != node.input_bindings.len()
326                || node.output_contracts.len() != node.output_bindings.len()
327                || node
328                    .input_ports
329                    .iter()
330                    .zip(&node.input_contracts)
331                    .any(|(name, contract)| name != &contract.name || !valid_compiled_port(contract))
332                || node
333                    .output_ports
334                    .iter()
335                    .zip(&node.output_contracts)
336                    .any(|(name, contract)| name != &contract.name || !valid_compiled_port(contract))
337                || node.input_ports.iter().any(|port| port.is_empty())
338                || node.output_ports.iter().any(|port| port.is_empty())
339                || node.input_ports.iter().collect::<BTreeSet<_>>().len()
340                    != node.input_ports.len()
341                || node.output_ports.iter().collect::<BTreeSet<_>>().len()
342                    != node.output_ports.len()
343                || node.input_bindings.iter().any(|binding| {
344                    matches!(binding, InputBinding::Buffer(buffer) if buffer.0 as usize >= plan.buffers.len())
345                })
346                || node.input_bindings.iter().any(|binding| {
347                    matches!(binding, InputBinding::Invocation(invocation) if *invocation as usize >= plan.invocation_ports.len())
348                })
349                || node.input_bindings.iter().any(|binding| {
350                    matches!(binding, InputBinding::Feedback(feedback) if feedback.0 as usize >= plan.feedback.len())
351                })
352                || node
353                    .input_bindings
354                    .iter()
355                    .zip(&node.input_contracts)
356                    .any(|(binding, contract)| match binding {
357                        InputBinding::Buffer(buffer) => {
358                            let buffer = &plan.buffers[buffer.0 as usize];
359                            contract.layout != buffer.layout
360                                || buffer.capacity_bytes > contract.max_bytes
361                        }
362                        _ => false,
363                    })
364                || node.output_bindings.iter().any(|binding| {
365                    matches!(binding, OutputBinding::Buffer(buffer) if buffer.0 as usize >= plan.buffers.len())
366                })
367                || node
368                    .output_bindings
369                    .iter()
370                    .zip(&node.output_contracts)
371                    .any(|(binding, contract)| match binding {
372                        OutputBinding::Buffer(buffer) => {
373                            let buffer = &plan.buffers[buffer.0 as usize];
374                            contract.layout != buffer.layout
375                                || buffer.capacity_bytes != contract.max_bytes
376                        }
377                        OutputBinding::Terminal => false,
378                    })
379                || node.resources.threads == 0
380                || node.failure.domains.iter().any(|domain| domain.is_empty())
381                || node.failure.domains.windows(2).any(|pair| pair[0] >= pair[1])
382                || (node.partiality == crate::Partiality::ExplicitGaps
383                    && node.failure.domains.is_empty())
384                || (node.effect == Effect::AtMostOnce && node.retry_limit > 0)
385                || !crate::compile::valid_state_contract(&node.state)
386                || node.subgraph_path.len() > limits.max_subgraph_depth
387                || node.input_bindings.iter().any(|binding| {
388                    matches!(binding, InputBinding::Buffer(buffer) if !plan.buffers[buffer.0 as usize].consumers.contains(&node.id))
389                })
390                || node.output_bindings.iter().any(|binding| {
391                    matches!(binding, OutputBinding::Buffer(buffer) if plan.buffers[buffer.0 as usize].producer != node.id)
392                })
393                || (conversion
394                    && (node.input_bindings.len() != 1
395                        || node.output_bindings.len() != 1
396                        || node.input_ports.as_slice() != ["input"]
397                        || node.output_ports.as_slice() != ["output"]
398                        || node.input_bindings.contains(&InputBinding::Absent)
399                        || node.input_bindings.iter().any(|binding| matches!(binding, InputBinding::Feedback(_)))
400                        || node.output_bindings.contains(&OutputBinding::Terminal)
401                        || node.partiality != crate::Partiality::Atomic
402                        || !node.failure.domains.is_empty()
403                        || node.effect != Effect::Pure
404                        || node.retry_limit != 0
405                        || node.state != crate::StateContract::stateless()
406                        || !node.subgraph_path.is_empty()))
407            {
408                return Err(PlanDecodeError::InvalidPlan);
409            }
410            if matches!(
411                node.state.scope,
412                crate::StateScope::Session | crate::StateScope::Durable
413            ) {
414                state_bytes = state_bytes
415                    .checked_add(node.state.max_bytes)
416                    .ok_or(PlanDecodeError::ResourceLimitExceeded)?;
417            }
418            if let Some(conversion) = &node.conversion {
419                let (InputBinding::Buffer(input), OutputBinding::Buffer(output)) =
420                    (node.input_bindings[0], node.output_bindings[0])
421                else {
422                    return Err(PlanDecodeError::InvalidPlan);
423                };
424                let input = &plan.buffers[input.0 as usize];
425                let output = &plan.buffers[output.0 as usize];
426                if input.layout != conversion.from
427                    || output.layout != conversion.to
428                    || input.capacity_bytes > conversion.max_input_bytes
429                    || output.capacity_bytes > conversion.max_output_bytes
430                    || node.input_contracts[0].semantic_type != conversion.semantic_type
431                    || node.output_contracts[0].semantic_type != conversion.semantic_type
432                    || node.input_contracts[0].layout != conversion.from
433                    || node.output_contracts[0].layout != conversion.to
434                    || node.input_contracts[0].max_bytes != input.capacity_bytes
435                    || node.output_contracts[0].max_bytes != output.capacity_bytes
436                {
437                    return Err(PlanDecodeError::InvalidPlan);
438                }
439            }
440            let mut workspace = node
441                .resources
442                .peak_bytes
443                .checked_add(node.resources.scratch_bytes)
444                .ok_or(PlanDecodeError::ResourceLimitExceeded)?;
445            if node.state.scope == crate::StateScope::Invocation {
446                workspace = workspace
447                    .checked_add(node.state.max_bytes)
448                    .ok_or(PlanDecodeError::ResourceLimitExceeded)?;
449            }
450            workspace_bytes = workspace_bytes.max(workspace);
451        }
452        if failure_entries > limits.max_contract_entries {
453            return Err(PlanDecodeError::LimitExceeded);
454        }
455        if used_invocations.len() != plan.invocation_ports.len()
456            || used_invocations
457                .iter()
458                .copied()
459                .ne(0..plan.invocation_ports.len() as u32)
460        {
461            return Err(PlanDecodeError::InvalidPlan);
462        }
463        if used_feedback.len() != plan.feedback.len()
464            || used_feedback
465                .iter()
466                .copied()
467                .ne((0..plan.feedback.len() as u32).map(crate::FeedbackId))
468        {
469            return Err(PlanDecodeError::InvalidPlan);
470        }
471        for buffer in &plan.buffers {
472            let producer = &plan.nodes[buffer.producer.0 as usize];
473            let producer_contract = producer
474                .output_bindings
475                .iter()
476                .position(|binding| *binding == OutputBinding::Buffer(buffer.id))
477                .and_then(|port| producer.output_contracts.get(port))
478                .ok_or(PlanDecodeError::InvalidPlan)?;
479            for consumer in &buffer.consumers {
480                let consumer = &plan.nodes[consumer.0 as usize];
481                let mut matched = false;
482                for (port, binding) in consumer.input_bindings.iter().enumerate() {
483                    if *binding == InputBinding::Buffer(buffer.id) {
484                        matched = true;
485                        if !crate::compile::compiled_port_contract_satisfies(
486                            producer_contract,
487                            &consumer.input_contracts[port],
488                        ) {
489                            return Err(PlanDecodeError::InvalidPlan);
490                        }
491                    }
492                }
493                if !matched {
494                    return Err(PlanDecodeError::InvalidPlan);
495                }
496            }
497        }
498        for (index, feedback) in plan.feedback.iter().enumerate() {
499            let output = plan
500                .nodes
501                .get(feedback.from_step.0 as usize)
502                .and_then(|node| node.output_contracts.get(feedback.from_port as usize));
503            let input = plan
504                .nodes
505                .get(feedback.to_step.0 as usize)
506                .and_then(|node| node.input_contracts.get(feedback.to_port as usize));
507            let expected_state_bytes = output.and_then(|contract| {
508                contract
509                    .max_bytes
510                    .checked_mul(u64::from(feedback.delay.invocations))
511            });
512            if feedback.id.0 as usize != index
513                || feedback.delay.invocations == 0
514                || feedback.state_bytes == 0
515                || feedback.from_step.0 as usize >= plan.nodes.len()
516                || feedback.to_step.0 as usize >= plan.nodes.len()
517                || output.is_none()
518                || input.is_none()
519                || (matches!(&feedback.delay.initial, crate::DelayInitial::Absent)
520                    && input.is_some_and(|contract| !contract.optional))
521                || plan.nodes[feedback.to_step.0 as usize].input_bindings[feedback.to_port as usize]
522                    != InputBinding::Feedback(feedback.id)
523                || expected_state_bytes != Some(feedback.state_bytes)
524                || !crate::compile::compiled_port_contract_satisfies(
525                    output.expect("checked above"),
526                    input.expect("checked above"),
527                )
528            {
529                return Err(PlanDecodeError::InvalidPlan);
530            }
531            state_bytes = state_bytes
532                .checked_add(feedback.state_bytes)
533                .ok_or(PlanDecodeError::ResourceLimitExceeded)?;
534        }
535        if state_bytes != plan.persistent_state_bytes
536            || (!plan.feedback.is_empty() && plan.session.is_none())
537            || plan.session.as_ref().is_some_and(|session| {
538                session.namespace.is_empty()
539                    || session.max_concurrent_sessions == 0
540                    || session.max_idle_millis == 0
541            })
542            || plan.nodes.iter().any(|node| {
543                matches!(
544                    node.state.scope,
545                    crate::StateScope::Session | crate::StateScope::Durable
546                ) && plan.session.is_none()
547            })
548        {
549            return Err(PlanDecodeError::InvalidPlan);
550        }
551        let mut produced_buffers = alloc::vec![0u8; plan.buffers.len()];
552        for binding in plan
553            .nodes
554            .iter()
555            .flat_map(|node| node.output_bindings.iter())
556        {
557            if let OutputBinding::Buffer(buffer) = binding {
558                produced_buffers[buffer.0 as usize] = produced_buffers[buffer.0 as usize]
559                    .checked_add(1)
560                    .ok_or(PlanDecodeError::InvalidBuffer)?;
561            }
562        }
563        if produced_buffers.iter().any(|count| *count != 1) {
564            return Err(PlanDecodeError::InvalidBuffer);
565        }
566        let expected_peak = buffer_bytes
567            .checked_add(workspace_bytes)
568            .ok_or(PlanDecodeError::ResourceLimitExceeded)?;
569        if plan.peak_bytes != expected_peak {
570            return Err(PlanDecodeError::ResourceLimitExceeded);
571        }
572        let expected = PlanId(hash_plan(&plan));
573        if plan.plan_id != expected {
574            return Err(PlanDecodeError::IdentityMismatch);
575        }
576        Ok(plan)
577    }
578
579    /// Decode and authorize an executable AOT plan against identity supplied
580    /// by a trusted compiler, signed manifest, or statically linked firmware.
581    /// `from_aot_bytes` alone performs structural validation, not authorization.
582    pub fn from_authorized_aot_bytes(
583        bytes: &[u8],
584        limits: PlanLimits,
585        authorization: PlanAuthorization,
586    ) -> Result<Self, PlanDecodeError> {
587        let plan = Self::from_aot_bytes(bytes, limits)?;
588        if plan.realm != authorization.expected_realm {
589            return Err(PlanDecodeError::RealmMismatch);
590        }
591        if plan.plan_id != authorization.expected_plan_id {
592            return Err(PlanDecodeError::UnauthorizedPlan);
593        }
594        Ok(plan)
595    }
596}
597
598#[cfg(test)]
599mod tests {
600    use alloc::vec;
601
602    use super::*;
603    use crate::model::{ExecutionRealm, GraphId};
604
605    fn minimal_plan() -> CompiledPlan {
606        let mut plan = CompiledPlan {
607            schema_version: 3,
608            graph_id: GraphId([1; 32]),
609            plan_id: PlanId([0; 32]),
610            realm: ExecutionRealm::McuAot,
611            order: vec![crate::NodeId(7)],
612            nodes: vec![crate::CompiledNode {
613                id: crate::StepId(0),
614                semantic_nodes: vec![crate::NodeId(7)],
615                semantic_types: vec![crate::NodeTypeRef {
616                    type_name: "test".into(),
617                    version: 1,
618                }],
619                semantic_configs: vec![alloc::collections::BTreeMap::new()],
620                kernel: crate::KernelId(11),
621                implementation_id: crate::ImplementationId([11; 32]),
622                resources: crate::ResourceEnvelope::bounded(0, 0, 1),
623                determinism: crate::Determinism::BitExact,
624                lowering: "test".into(),
625                conversion: None,
626                input_ports: vec![],
627                output_ports: vec!["out".into()],
628                input_contracts: vec![],
629                output_contracts: vec![crate::CompiledPortContract::opaque(
630                    "out",
631                    "test",
632                    crate::Layout::Canonical,
633                    1,
634                )],
635                input_bindings: vec![],
636                output_bindings: vec![crate::OutputBinding::Terminal],
637                partiality: crate::Partiality::Atomic,
638                failure: crate::FailureContract { domains: vec![] },
639                effect: crate::Effect::Pure,
640                retry_limit: 0,
641                state: crate::StateContract::stateless(),
642                subgraph_path: vec![],
643            }],
644            buffers: vec![],
645            feedback: vec![],
646            invocation_ports: vec![],
647            propagated_proofs: vec![],
648            propagated_policy: vec![],
649            resulting_fidelity: u16::MAX,
650            peak_bytes: 0,
651            persistent_state_bytes: 0,
652            session: None,
653        };
654        plan.plan_id = PlanId(hash_plan(&plan));
655        plan
656    }
657
658    #[test]
659    fn bgp2_magic_is_never_reinterpreted_as_bgp3() {
660        let mut bytes = minimal_plan().to_aot_bytes().unwrap();
661        bytes[..4].copy_from_slice(b"BGP2");
662        assert_eq!(
663            CompiledPlan::from_aot_bytes(&bytes, PlanLimits::default()),
664            Err(PlanDecodeError::BadMagic)
665        );
666    }
667
668    #[test]
669    fn self_consistent_port_and_state_forgery_is_structurally_rejected() {
670        let mut port_forgery = minimal_plan();
671        port_forgery.nodes[0].output_contracts[0].name = "different".into();
672        port_forgery.plan_id = PlanId(hash_plan(&port_forgery));
673        assert_eq!(
674            CompiledPlan::from_aot_bytes(
675                &port_forgery.to_aot_bytes().unwrap(),
676                PlanLimits::default(),
677            ),
678            Err(PlanDecodeError::InvalidPlan)
679        );
680
681        let mut state_forgery = minimal_plan();
682        state_forgery.persistent_state_bytes = 1;
683        state_forgery.plan_id = PlanId(hash_plan(&state_forgery));
684        assert_eq!(
685            CompiledPlan::from_aot_bytes(
686                &state_forgery.to_aot_bytes().unwrap(),
687                PlanLimits::default(),
688            ),
689            Err(PlanDecodeError::InvalidPlan)
690        );
691
692        let mut rewire_forgery = minimal_plan();
693        rewire_forgery.order.push(crate::NodeId(8));
694        rewire_forgery.nodes[0].output_bindings[0] =
695            crate::OutputBinding::Buffer(crate::BufferId(0));
696        let mut sink = rewire_forgery.nodes[0].clone();
697        sink.id = crate::StepId(1);
698        sink.semantic_nodes = vec![crate::NodeId(8)];
699        sink.input_ports = vec!["in".into()];
700        sink.output_ports = vec!["out".into()];
701        sink.input_contracts = vec![crate::CompiledPortContract::opaque(
702            "in",
703            "test",
704            crate::Layout::Canonical,
705            1,
706        )];
707        sink.output_contracts = vec![crate::CompiledPortContract::opaque(
708            "out",
709            "test",
710            crate::Layout::Canonical,
711            1,
712        )];
713        sink.input_bindings = vec![InputBinding::Buffer(crate::BufferId(0))];
714        sink.output_bindings = vec![OutputBinding::Terminal];
715        rewire_forgery.nodes.push(sink);
716        rewire_forgery.buffers.push(crate::BufferPlan {
717            id: crate::BufferId(0),
718            layout: crate::Layout::Canonical,
719            capacity_bytes: 1,
720            producer: crate::StepId(0),
721            consumers: vec![crate::StepId(1)],
722            last_consumer: crate::StepId(1),
723            aliases: None,
724        });
725        rewire_forgery.peak_bytes = 1;
726        rewire_forgery.plan_id = PlanId(hash_plan(&rewire_forgery));
727        assert!(
728            CompiledPlan::from_aot_bytes(
729                &rewire_forgery.to_aot_bytes().unwrap(),
730                PlanLimits::default(),
731            )
732            .is_ok()
733        );
734
735        rewire_forgery.nodes[1].input_contracts[0].semantic_type = "different".into();
736        rewire_forgery.plan_id = PlanId(hash_plan(&rewire_forgery));
737        assert_eq!(
738            CompiledPlan::from_aot_bytes(
739                &rewire_forgery.to_aot_bytes().unwrap(),
740                PlanLimits::default(),
741            ),
742            Err(PlanDecodeError::InvalidPlan)
743        );
744    }
745
746    #[test]
747    fn empty_compiled_contract_names_are_rejected() {
748        let mut port_contract = minimal_plan();
749        port_contract.nodes[0].output_contracts[0]
750            .policy
751            .adds
752            .push(String::new());
753        port_contract.plan_id = PlanId(hash_plan(&port_contract));
754        assert_eq!(
755            CompiledPlan::from_aot_bytes(
756                &port_contract.to_aot_bytes().unwrap(),
757                PlanLimits::default(),
758            ),
759            Err(PlanDecodeError::InvalidPlan)
760        );
761
762        let mut propagated_contract = minimal_plan();
763        propagated_contract.propagated_proofs.push(String::new());
764        propagated_contract.plan_id = PlanId(hash_plan(&propagated_contract));
765        assert_eq!(
766            CompiledPlan::from_aot_bytes(
767                &propagated_contract.to_aot_bytes().unwrap(),
768                PlanLimits::default(),
769            ),
770            Err(PlanDecodeError::InvalidPlan)
771        );
772
773        let mut propagated_policy = minimal_plan();
774        propagated_policy.propagated_policy.push(String::new());
775        propagated_policy.plan_id = PlanId(hash_plan(&propagated_policy));
776        assert_eq!(
777            CompiledPlan::from_aot_bytes(
778                &propagated_policy.to_aot_bytes().unwrap(),
779                PlanLimits::default(),
780            ),
781            Err(PlanDecodeError::InvalidPlan)
782        );
783    }
784
785    #[test]
786    fn subgraph_depth_limit_is_enforced_during_structural_decode() {
787        let mut plan = minimal_plan();
788        plan.nodes[0].subgraph_path = vec![crate::SubgraphId([1; 32]), crate::SubgraphId([2; 32])];
789        plan.plan_id = PlanId(hash_plan(&plan));
790        assert_eq!(
791            CompiledPlan::from_authorized_aot_bytes(
792                &plan.to_aot_bytes().unwrap(),
793                PlanLimits {
794                    max_subgraph_depth: 1,
795                    ..PlanLimits::default()
796                },
797                PlanAuthorization {
798                    expected_realm: plan.realm,
799                    expected_plan_id: plan.plan_id,
800                },
801            ),
802            Err(PlanDecodeError::InvalidPlan)
803        );
804    }
805
806    #[test]
807    fn plan_round_trip_and_tamper_rejection() {
808        let mut plan = CompiledPlan {
809            schema_version: 3,
810            graph_id: GraphId([1; 32]),
811            plan_id: PlanId([0; 32]),
812            realm: ExecutionRealm::McuAot,
813            order: vec![crate::NodeId(7)],
814            nodes: vec![crate::CompiledNode {
815                id: crate::StepId(0),
816                semantic_nodes: vec![crate::NodeId(7)],
817                semantic_types: vec![crate::NodeTypeRef {
818                    type_name: "test".into(),
819                    version: 1,
820                }],
821                semantic_configs: vec![alloc::collections::BTreeMap::new()],
822                kernel: crate::KernelId(11),
823                implementation_id: crate::ImplementationId([11; 32]),
824                resources: crate::ResourceEnvelope::bounded(0, 0, 1),
825                determinism: crate::Determinism::BitExact,
826                lowering: "test".into(),
827                conversion: None,
828                input_ports: vec![],
829                output_ports: vec!["out".into()],
830                input_contracts: vec![],
831                output_contracts: vec![crate::CompiledPortContract::opaque(
832                    "out",
833                    "test",
834                    crate::Layout::Canonical,
835                    1,
836                )],
837                input_bindings: vec![],
838                output_bindings: vec![crate::OutputBinding::Terminal],
839                partiality: crate::Partiality::Atomic,
840                failure: crate::FailureContract { domains: vec![] },
841                effect: crate::Effect::Pure,
842                retry_limit: 0,
843                state: crate::StateContract::stateless(),
844                subgraph_path: vec![],
845            }],
846            buffers: vec![],
847            feedback: vec![],
848            invocation_ports: vec![],
849            propagated_proofs: vec![],
850            propagated_policy: vec![],
851            resulting_fidelity: u16::MAX,
852            peak_bytes: 0,
853            persistent_state_bytes: 0,
854            session: None,
855        };
856        plan.plan_id = PlanId(hash_plan(&plan));
857        let bytes = plan.to_aot_bytes().unwrap();
858        assert_eq!(
859            CompiledPlan::from_aot_bytes(&bytes, PlanLimits::default()).unwrap(),
860            plan
861        );
862        let mut tampered = bytes;
863        *tampered.last_mut().unwrap() ^= 1;
864        assert!(CompiledPlan::from_aot_bytes(&tampered, PlanLimits::default()).is_err());
865    }
866
867    #[test]
868    fn manifest_authorization_binds_realm_and_plan_identity() {
869        let mut plan = CompiledPlan {
870            schema_version: 3,
871            graph_id: GraphId([1; 32]),
872            plan_id: PlanId([0; 32]),
873            realm: ExecutionRealm::McuAot,
874            order: vec![crate::NodeId(7)],
875            nodes: vec![crate::CompiledNode {
876                id: crate::StepId(0),
877                semantic_nodes: vec![crate::NodeId(7)],
878                semantic_types: vec![crate::NodeTypeRef {
879                    type_name: "test".into(),
880                    version: 1,
881                }],
882                semantic_configs: vec![alloc::collections::BTreeMap::new()],
883                kernel: crate::KernelId(11),
884                implementation_id: crate::ImplementationId([11; 32]),
885                resources: crate::ResourceEnvelope::bounded(0, 0, 1),
886                determinism: crate::Determinism::BitExact,
887                lowering: "test".into(),
888                conversion: None,
889                input_ports: vec![],
890                output_ports: vec!["out".into()],
891                input_contracts: vec![],
892                output_contracts: vec![crate::CompiledPortContract::opaque(
893                    "out",
894                    "test",
895                    crate::Layout::Canonical,
896                    1,
897                )],
898                input_bindings: vec![],
899                output_bindings: vec![crate::OutputBinding::Terminal],
900                partiality: crate::Partiality::Atomic,
901                failure: crate::FailureContract { domains: vec![] },
902                effect: crate::Effect::Pure,
903                retry_limit: 0,
904                state: crate::StateContract::stateless(),
905                subgraph_path: vec![],
906            }],
907            buffers: vec![],
908            feedback: vec![],
909            invocation_ports: vec![],
910            propagated_proofs: vec![],
911            propagated_policy: vec![],
912            resulting_fidelity: u16::MAX,
913            peak_bytes: 0,
914            persistent_state_bytes: 0,
915            session: None,
916        };
917        plan.plan_id = PlanId(hash_plan(&plan));
918        let bytes = plan.to_aot_bytes().unwrap();
919        assert_eq!(
920            CompiledPlan::from_authorized_aot_bytes(
921                &bytes,
922                PlanLimits::default(),
923                PlanAuthorization {
924                    expected_realm: ExecutionRealm::HostStream,
925                    expected_plan_id: plan.plan_id,
926                },
927            ),
928            Err(PlanDecodeError::RealmMismatch)
929        );
930        assert_eq!(
931            CompiledPlan::from_authorized_aot_bytes(
932                &bytes,
933                PlanLimits::default(),
934                PlanAuthorization {
935                    expected_realm: ExecutionRealm::McuAot,
936                    expected_plan_id: PlanId([9; 32]),
937                },
938            ),
939            Err(PlanDecodeError::UnauthorizedPlan)
940        );
941    }
942
943    #[test]
944    fn oversized_input_fails_before_decode() {
945        let bytes = vec![0; 9];
946        assert_eq!(
947            CompiledPlan::from_aot_bytes(
948                &bytes,
949                PlanLimits {
950                    max_bytes: 8,
951                    ..PlanLimits::default()
952                }
953            ),
954            Err(PlanDecodeError::TooLarge)
955        );
956    }
957
958    #[test]
959    fn declared_peak_memory_is_bounded_before_execution() {
960        let mut plan = CompiledPlan {
961            schema_version: 3,
962            graph_id: GraphId([1; 32]),
963            plan_id: PlanId([0; 32]),
964            realm: ExecutionRealm::McuAot,
965            order: vec![crate::NodeId(7)],
966            nodes: vec![crate::CompiledNode {
967                id: crate::StepId(0),
968                semantic_nodes: vec![crate::NodeId(7)],
969                semantic_types: vec![crate::NodeTypeRef {
970                    type_name: "test".into(),
971                    version: 1,
972                }],
973                semantic_configs: vec![alloc::collections::BTreeMap::new()],
974                kernel: crate::KernelId(11),
975                implementation_id: crate::ImplementationId([11; 32]),
976                resources: crate::ResourceEnvelope::bounded(0, 0, 1),
977                determinism: crate::Determinism::BitExact,
978                lowering: "test".into(),
979                conversion: None,
980                input_ports: vec![],
981                output_ports: vec!["out".into()],
982                input_contracts: vec![],
983                output_contracts: vec![crate::CompiledPortContract::opaque(
984                    "out",
985                    "test",
986                    crate::Layout::Canonical,
987                    1,
988                )],
989                input_bindings: vec![],
990                output_bindings: vec![crate::OutputBinding::Terminal],
991                partiality: crate::Partiality::Atomic,
992                failure: crate::FailureContract { domains: vec![] },
993                effect: crate::Effect::Pure,
994                retry_limit: 0,
995                state: crate::StateContract::stateless(),
996                subgraph_path: vec![],
997            }],
998            buffers: vec![],
999            feedback: vec![],
1000            invocation_ports: vec![],
1001            propagated_proofs: vec![],
1002            propagated_policy: vec![],
1003            resulting_fidelity: u16::MAX,
1004            peak_bytes: 4096,
1005            persistent_state_bytes: 0,
1006            session: None,
1007        };
1008        plan.plan_id = PlanId(hash_plan(&plan));
1009        let bytes = plan.to_aot_bytes().unwrap();
1010        assert_eq!(
1011            CompiledPlan::from_aot_bytes(
1012                &bytes,
1013                PlanLimits {
1014                    max_peak_bytes: 1024,
1015                    ..PlanLimits::default()
1016                }
1017            ),
1018            Err(PlanDecodeError::ResourceLimitExceeded)
1019        );
1020    }
1021}