Skip to main content

rns_hooks/
manager.rs

1use crate::arena;
2use crate::engine_access::{EngineAccess, NullEngine};
3use crate::error::HookError;
4use crate::hooks::HookContext;
5use crate::host_fns;
6use crate::program::LoadedProgram;
7use crate::result::{ExecuteResult, HookResult, Verdict};
8use crate::runtime::{StoreData, WasmRuntime};
9use wasmtime::{Linker, Store};
10
11/// ABI version the host expects from compiled hook modules.
12const HOST_ABI_VERSION: i32 = rns_hooks_abi::ABI_VERSION;
13
14/// Central manager for WASM hook execution.
15///
16/// Owns the wasmtime runtime and pre-configured linker. Programs are stored
17/// in `HookSlot`s (one per hook point); the manager provides execution.
18pub struct HookManager {
19    runtime: WasmRuntime,
20    linker: Linker<StoreData>,
21}
22
23impl HookManager {
24    pub fn new() -> Result<Self, HookError> {
25        let runtime = WasmRuntime::new().map_err(|e| HookError::CompileError(e.to_string()))?;
26        let mut linker = Linker::new(runtime.engine());
27        host_fns::register_host_functions(&mut linker)
28            .map_err(|e| HookError::CompileError(e.to_string()))?;
29        Ok(HookManager { runtime, linker })
30    }
31
32    /// Compile WASM bytes into a LoadedProgram.
33    ///
34    /// Validates that the module exports `__rns_abi_version` returning the
35    /// expected ABI version before accepting it.
36    pub fn compile(
37        &self,
38        name: String,
39        bytes: &[u8],
40        priority: i32,
41    ) -> Result<LoadedProgram, HookError> {
42        let module = self
43            .runtime
44            .compile(bytes)
45            .map_err(|e| HookError::CompileError(e.to_string()))?;
46        self.validate_abi_version(&name, &module)?;
47        Ok(LoadedProgram::new(name, module, priority))
48    }
49
50    /// Check that the module exports `__rns_abi_version() -> i32` and that
51    /// the returned value matches [`HOST_ABI_VERSION`].
52    fn validate_abi_version(&self, name: &str, module: &wasmtime::Module) -> Result<(), HookError> {
53        // Check if the export exists in the module's type information.
54        let has_export = module.exports().any(|e| e.name() == "__rns_abi_version");
55        if !has_export {
56            return Err(HookError::AbiVersionMismatch {
57                hook_name: name.to_string(),
58                expected: HOST_ABI_VERSION,
59                found: None,
60            });
61        }
62
63        // Instantiate the module to call the function and read the version.
64        static NULL_ENGINE: NullEngine = NullEngine;
65        let mut store = Store::new(
66            self.runtime.engine(),
67            StoreData {
68                engine_access: &NULL_ENGINE as *const dyn EngineAccess,
69                now: 0.0,
70                injected_actions: Vec::new(),
71                log_messages: Vec::new(),
72                provider_events: Vec::new(),
73                provider_events_enabled: false,
74            },
75        );
76        store
77            .set_fuel(self.runtime.fuel())
78            .map_err(|e| HookError::CompileError(e.to_string()))?;
79
80        let instance = self
81            .linker
82            .instantiate(&mut store, module)
83            .map_err(|e| HookError::InstantiationError(e.to_string()))?;
84
85        let func = instance
86            .get_typed_func::<(), i32>(&mut store, "__rns_abi_version")
87            .map_err(|e| {
88                HookError::CompileError(format!("__rns_abi_version has wrong signature: {}", e))
89            })?;
90
91        let version = func
92            .call(&mut store, ())
93            .map_err(|e| HookError::Trap(format!("__rns_abi_version trapped: {}", e)))?;
94
95        if version != HOST_ABI_VERSION {
96            return Err(HookError::AbiVersionMismatch {
97                hook_name: name.to_string(),
98                expected: HOST_ABI_VERSION,
99                found: Some(version),
100            });
101        }
102
103        Ok(())
104    }
105
106    /// Compile a WASM file from disk.
107    pub fn load_file(
108        &self,
109        name: String,
110        path: &std::path::Path,
111        priority: i32,
112    ) -> Result<LoadedProgram, HookError> {
113        let bytes = std::fs::read(path)?;
114        self.compile(name, &bytes, priority)
115    }
116
117    /// Execute a single program against a hook context. Returns an `ExecuteResult`
118    /// containing the hook result, any injected actions, and modified data (all
119    /// extracted from WASM memory before the store is dropped). Returns `None`
120    /// on trap/fuel exhaustion (fail-open).
121    ///
122    /// If `data_override` is provided (from a previous Modify verdict in a chain),
123    /// it replaces the packet data region in the arena after writing the context.
124    ///
125    /// The store and instance are cached in the program for cross-call state
126    /// persistence (WASM linear memory survives across invocations). On each call
127    /// we reset fuel and per-call StoreData fields but keep the WASM globals and
128    /// memory intact.
129    pub fn execute_program(
130        &self,
131        program: &mut LoadedProgram,
132        ctx: &HookContext,
133        engine_access: &dyn EngineAccess,
134        now: f64,
135        data_override: Option<&[u8]>,
136    ) -> Option<ExecuteResult> {
137        self.execute_program_with_provider_events(
138            program,
139            ctx,
140            engine_access,
141            now,
142            false,
143            data_override,
144        )
145    }
146
147    pub fn execute_program_with_provider_events(
148        &self,
149        program: &mut LoadedProgram,
150        ctx: &HookContext,
151        engine_access: &dyn EngineAccess,
152        now: f64,
153        provider_events_enabled: bool,
154        data_override: Option<&[u8]>,
155    ) -> Option<ExecuteResult> {
156        if !program.enabled {
157            return None;
158        }
159
160        // Safety: transmute erases the lifetime on the fat pointer. The pointer
161        // is only dereferenced during this function call, while the borrow is valid.
162        let engine_access_ptr: *const dyn EngineAccess =
163            unsafe { std::mem::transmute(engine_access as *const dyn EngineAccess) };
164
165        // Take the cached store+instance out of program (or create fresh).
166        // We take ownership to avoid borrow-checker conflicts with program.record_*().
167        let (mut store, instance) = if let Some(cached) = program.cached.take() {
168            let (mut s, i) = cached;
169            // Reset per-call state: fuel, engine_access, injected_actions, log_messages
170            s.data_mut()
171                .reset_per_call(engine_access_ptr, now, provider_events_enabled);
172            if let Err(e) = s.set_fuel(self.runtime.fuel()) {
173                log::warn!("failed to set fuel for hook '{}': {}", program.name, e);
174                program.cached = Some((s, i));
175                return None;
176            }
177            (s, i)
178        } else {
179            let store_data = StoreData {
180                engine_access: engine_access_ptr,
181                now,
182                injected_actions: Vec::new(),
183                log_messages: Vec::new(),
184                provider_events: Vec::new(),
185                provider_events_enabled,
186            };
187
188            let mut store = Store::new(self.runtime.engine(), store_data);
189            if let Err(e) = store.set_fuel(self.runtime.fuel()) {
190                log::warn!("failed to set fuel for hook '{}': {}", program.name, e);
191                return None;
192            }
193
194            let instance = match self.linker.instantiate(&mut store, &program.module) {
195                Ok(inst) => inst,
196                Err(e) => {
197                    log::warn!("failed to instantiate hook '{}': {}", program.name, e);
198                    program.record_trap();
199                    return None;
200                }
201            };
202
203            (store, instance)
204        };
205
206        // Write context into guest memory
207        let memory = match instance.get_memory(&mut store, "memory") {
208            Some(mem) => mem,
209            None => {
210                log::warn!("hook '{}' has no exported memory", program.name);
211                program.record_trap();
212                program.cached = Some((store, instance));
213                return None;
214            }
215        };
216
217        if let Err(e) = arena::write_context(&memory, &mut store, ctx) {
218            log::warn!("failed to write context for hook '{}': {}", program.name, e);
219            program.record_trap();
220            program.cached = Some((store, instance));
221            return None;
222        }
223
224        // If a previous hook in the chain returned Modify, override the packet data
225        if let Some(override_data) = data_override {
226            if let Err(e) = arena::write_data_override(&memory, &mut store, override_data) {
227                log::warn!(
228                    "failed to write data override for hook '{}': {}",
229                    program.name,
230                    e
231                );
232                // Non-fatal: continue with original data
233            }
234        }
235
236        // Call the exported hook function
237        let func = match instance.get_typed_func::<i32, i32>(&mut store, &program.export_name) {
238            Ok(f) => f,
239            Err(e) => {
240                log::warn!(
241                    "hook '{}' missing export '{}': {}",
242                    program.name,
243                    program.export_name,
244                    e
245                );
246                program.record_trap();
247                program.cached = Some((store, instance));
248                return None;
249            }
250        };
251
252        let result_offset = match func.call(&mut store, arena::ARENA_BASE as i32) {
253            Ok(offset) => offset,
254            Err(e) => {
255                // Fail-open: trap or fuel exhaustion → continue
256                let auto_disabled = program.record_trap();
257                if auto_disabled {
258                    log::error!(
259                        "hook '{}' auto-disabled after {} consecutive traps",
260                        program.name,
261                        program.consecutive_traps
262                    );
263                } else {
264                    log::warn!("hook '{}' trapped: {}", program.name, e);
265                }
266                program.cached = Some((store, instance));
267                return None;
268            }
269        };
270
271        // Read result from guest memory
272        let ret = match arena::read_result(&memory, &store, result_offset as usize) {
273            Ok(result) => {
274                program.record_success();
275
276                // Extract modified data from WASM memory
277                let modified_data = if Verdict::from_u32(result.verdict) == Some(Verdict::Modify) {
278                    arena::read_modified_data(&memory, &store, &result)
279                } else {
280                    None
281                };
282
283                // Extract injected actions from the store
284                let injected_actions = std::mem::take(&mut store.data_mut().injected_actions);
285                let provider_events = std::mem::take(&mut store.data_mut().provider_events)
286                    .into_iter()
287                    .map(|event| crate::result::EmittedProviderEvent {
288                        hook_name: program.name.clone(),
289                        payload_type: event.payload_type,
290                        payload: event.payload,
291                    })
292                    .collect();
293
294                Some(ExecuteResult {
295                    hook_result: Some(result),
296                    injected_actions,
297                    provider_events,
298                    modified_data,
299                })
300            }
301            Err(e) => {
302                log::warn!("hook '{}' returned invalid result: {}", program.name, e);
303                program.record_trap();
304                None
305            }
306        };
307
308        // Put the store+instance back for next call
309        program.cached = Some((store, instance));
310        ret
311    }
312
313    /// Run a chain of programs. Stops on Drop or Halt, continues on Continue or Modify.
314    /// Returns an `ExecuteResult` accumulating all injected actions across the chain
315    /// and the last meaningful hook result (Drop/Halt/Modify), or None if all continued.
316    ///
317    /// When a hook returns Modify with modified data, subsequent hooks in the chain
318    /// receive the modified data (only applicable to Packet contexts).
319    pub fn run_chain(
320        &self,
321        programs: &mut [LoadedProgram],
322        ctx: &HookContext,
323        engine_access: &dyn EngineAccess,
324        now: f64,
325    ) -> Option<ExecuteResult> {
326        self.run_chain_with_provider_events(programs, ctx, engine_access, now, false)
327    }
328
329    pub fn run_chain_with_provider_events(
330        &self,
331        programs: &mut [LoadedProgram],
332        ctx: &HookContext,
333        engine_access: &dyn EngineAccess,
334        now: f64,
335        provider_events_enabled: bool,
336    ) -> Option<ExecuteResult> {
337        let mut accumulated_actions = Vec::new();
338        let mut accumulated_provider_events = Vec::new();
339        let mut last_result: Option<HookResult> = None;
340        let mut last_modified_data: Option<Vec<u8>> = None;
341        let is_packet_ctx = matches!(ctx, HookContext::Packet { .. });
342
343        for program in programs.iter_mut() {
344            if !program.enabled {
345                continue;
346            }
347            let override_ref = if is_packet_ctx {
348                last_modified_data.as_deref()
349            } else {
350                None
351            };
352            if let Some(exec_result) = self.execute_program_with_provider_events(
353                program,
354                ctx,
355                engine_access,
356                now,
357                provider_events_enabled,
358                override_ref,
359            ) {
360                accumulated_actions.extend(exec_result.injected_actions);
361                accumulated_provider_events.extend(exec_result.provider_events);
362
363                if let Some(ref result) = exec_result.hook_result {
364                    let verdict = Verdict::from_u32(result.verdict);
365                    match verdict {
366                        Some(Verdict::Drop) | Some(Verdict::Halt) => {
367                            return Some(ExecuteResult {
368                                hook_result: exec_result.hook_result,
369                                injected_actions: accumulated_actions,
370                                provider_events: accumulated_provider_events,
371                                modified_data: exec_result.modified_data.or(last_modified_data),
372                            });
373                        }
374                        Some(Verdict::Modify) => {
375                            last_result = exec_result.hook_result;
376                            if is_packet_ctx {
377                                if let Some(data) = exec_result.modified_data {
378                                    last_modified_data = Some(data);
379                                }
380                            }
381                        }
382                        _ => {} // Continue → keep going
383                    }
384                }
385            }
386        }
387
388        if last_result.is_some()
389            || !accumulated_actions.is_empty()
390            || !accumulated_provider_events.is_empty()
391        {
392            Some(ExecuteResult {
393                hook_result: last_result,
394                injected_actions: accumulated_actions,
395                provider_events: accumulated_provider_events,
396                modified_data: last_modified_data,
397            })
398        } else {
399            None
400        }
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407    use crate::engine_access::NullEngine;
408
409    fn make_manager() -> HookManager {
410        HookManager::new().expect("failed to create HookManager")
411    }
412
413    /// WAT module that returns Continue (verdict=0).
414    const WAT_CONTINUE: &str = r#"
415        (module
416            (memory (export "memory") 1)
417            (func (export "__rns_abi_version") (result i32) (i32.const 1))
418            (func (export "on_hook") (param i32) (result i32)
419                ;; Write HookResult at offset 0x2000
420                ;; verdict = 0 (Continue)
421                (i32.store (i32.const 0x2000) (i32.const 0))
422                ;; modified_data_offset = 0
423                (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
424                ;; modified_data_len = 0
425                (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
426                ;; inject_actions_offset = 0
427                (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
428                ;; inject_actions_count = 0
429                (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
430                ;; log_offset = 0
431                (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
432                ;; log_len = 0
433                (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
434                (i32.const 0x2000)
435            )
436        )
437    "#;
438
439    /// WAT module that returns Drop (verdict=1).
440    const WAT_DROP: &str = r#"
441        (module
442            (memory (export "memory") 1)
443            (func (export "__rns_abi_version") (result i32) (i32.const 1))
444            (func (export "on_hook") (param i32) (result i32)
445                (i32.store (i32.const 0x2000) (i32.const 1))
446                (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
447                (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
448                (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
449                (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
450                (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
451                (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
452                (i32.const 0x2000)
453            )
454        )
455    "#;
456
457    /// WAT module that traps immediately.
458    const WAT_TRAP: &str = r#"
459        (module
460            (memory (export "memory") 1)
461            (func (export "__rns_abi_version") (result i32) (i32.const 1))
462            (func (export "on_hook") (param i32) (result i32)
463                unreachable
464            )
465        )
466    "#;
467
468    /// WAT module with infinite loop (will exhaust fuel).
469    const WAT_INFINITE: &str = r#"
470        (module
471            (memory (export "memory") 1)
472            (func (export "__rns_abi_version") (result i32) (i32.const 1))
473            (func (export "on_hook") (param i32) (result i32)
474                (loop $inf (br $inf))
475                (i32.const 0)
476            )
477        )
478    "#;
479
480    /// WAT module that calls host_has_path and drops if path exists.
481    const WAT_HOST_HAS_PATH: &str = r#"
482        (module
483            (import "env" "host_has_path" (func $has_path (param i32) (result i32)))
484            (memory (export "memory") 1)
485            (func (export "__rns_abi_version") (result i32) (i32.const 1))
486            (func (export "on_hook") (param $ctx_ptr i32) (result i32)
487                ;; Check if path exists for a 16-byte dest at offset 0x3000
488                ;; (we'll write the dest hash there in the test)
489                (if (call $has_path (i32.const 0x3000))
490                    (then
491                        ;; Drop
492                        (i32.store (i32.const 0x2000) (i32.const 1))
493                    )
494                    (else
495                        ;; Continue
496                        (i32.store (i32.const 0x2000) (i32.const 0))
497                    )
498                )
499                (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
500                (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
501                (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
502                (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
503                (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
504                (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
505                (i32.const 0x2000)
506            )
507        )
508    "#;
509
510    #[test]
511    fn pass_through() {
512        let mgr = make_manager();
513        let mut prog = mgr
514            .compile("test".into(), WAT_CONTINUE.as_bytes(), 0)
515            .unwrap();
516        let ctx = HookContext::Tick;
517        let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
518        // Continue → Some with verdict=0
519        let exec = result.unwrap();
520        let r = exec.hook_result.unwrap();
521        assert_eq!(r.verdict, Verdict::Continue as u32);
522    }
523
524    #[test]
525    fn drop_hook() {
526        let mgr = make_manager();
527        let mut prog = mgr
528            .compile("dropper".into(), WAT_DROP.as_bytes(), 0)
529            .unwrap();
530        let ctx = HookContext::Tick;
531        let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
532        let exec = result.unwrap();
533        let r = exec.hook_result.unwrap();
534        assert!(r.is_drop());
535    }
536
537    #[test]
538    fn trap_failopen() {
539        let mgr = make_manager();
540        let mut prog = mgr.compile("trap".into(), WAT_TRAP.as_bytes(), 0).unwrap();
541        let ctx = HookContext::Tick;
542        let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
543        assert!(result.is_none());
544        assert_eq!(prog.consecutive_traps, 1);
545        assert!(prog.enabled);
546    }
547
548    #[test]
549    fn auto_disable() {
550        let mgr = make_manager();
551        let mut prog = mgr.compile("bad".into(), WAT_TRAP.as_bytes(), 0).unwrap();
552        let ctx = HookContext::Tick;
553        for _ in 0..10 {
554            let _ = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
555        }
556        assert!(!prog.enabled);
557        assert_eq!(prog.consecutive_traps, 10);
558    }
559
560    #[test]
561    fn fuel_exhaustion() {
562        let mgr = make_manager();
563        let mut prog = mgr
564            .compile("loop".into(), WAT_INFINITE.as_bytes(), 0)
565            .unwrap();
566        let ctx = HookContext::Tick;
567        let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
568        // Should fail-open (fuel exhausted = trap)
569        assert!(result.is_none());
570        assert_eq!(prog.consecutive_traps, 1);
571    }
572
573    #[test]
574    fn chain_ordering() {
575        let mgr = make_manager();
576        let high = mgr
577            .compile("high".into(), WAT_DROP.as_bytes(), 100)
578            .unwrap();
579        let low = mgr
580            .compile("low".into(), WAT_CONTINUE.as_bytes(), 0)
581            .unwrap();
582        // Programs sorted by priority desc: high first
583        let mut programs = vec![high, low];
584        // Sort descending by priority (as attach would do)
585        programs.sort_by(|a, b| b.priority.cmp(&a.priority));
586
587        let ctx = HookContext::Tick;
588        let result = mgr.run_chain(&mut programs, &ctx, &NullEngine, 0.0);
589        // High priority drops → chain stops
590        let exec = result.unwrap();
591        let r = exec.hook_result.unwrap();
592        assert!(r.is_drop());
593    }
594
595    #[test]
596    fn attach_detach() {
597        use crate::hooks::HookSlot;
598
599        let mgr = make_manager();
600        let mut slot = HookSlot {
601            programs: Vec::new(),
602            runner: crate::hooks::hook_noop,
603        };
604
605        let p1 = mgr
606            .compile("alpha".into(), WAT_CONTINUE.as_bytes(), 10)
607            .unwrap();
608        let p2 = mgr.compile("beta".into(), WAT_DROP.as_bytes(), 20).unwrap();
609
610        slot.attach(p1);
611        assert_eq!(slot.programs.len(), 1);
612        assert!(!std::ptr::eq(
613            slot.runner as *const (),
614            crate::hooks::hook_noop as *const (),
615        ));
616
617        slot.attach(p2);
618        assert_eq!(slot.programs.len(), 2);
619        // Sorted descending: beta(20) before alpha(10)
620        assert_eq!(slot.programs[0].name, "beta");
621        assert_eq!(slot.programs[1].name, "alpha");
622
623        let removed = slot.detach("beta");
624        assert!(removed.is_some());
625        assert_eq!(slot.programs.len(), 1);
626        assert_eq!(slot.programs[0].name, "alpha");
627
628        let removed2 = slot.detach("alpha");
629        assert!(removed2.is_some());
630        assert!(slot.programs.is_empty());
631        assert_eq!(
632            slot.runner as *const () as usize,
633            crate::hooks::hook_noop as *const () as usize
634        );
635    }
636
637    #[test]
638    fn host_has_path() {
639        use crate::engine_access::EngineAccess;
640
641        struct MockEngine;
642        impl EngineAccess for MockEngine {
643            fn has_path(&self, _dest: &[u8; 16]) -> bool {
644                true
645            }
646            fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
647                None
648            }
649            fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
650                None
651            }
652            fn is_blackholed(&self, _: &[u8; 16]) -> bool {
653                false
654            }
655            fn interface_name(&self, _: u64) -> Option<String> {
656                None
657            }
658            fn interface_mode(&self, _: u64) -> Option<u8> {
659                None
660            }
661            fn identity_hash(&self) -> Option<[u8; 16]> {
662                None
663            }
664            fn announce_rate(&self, _: u64) -> Option<i32> {
665                None
666            }
667            fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
668                None
669            }
670        }
671
672        let mgr = make_manager();
673        let mut prog = mgr
674            .compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
675            .unwrap();
676        let ctx = HookContext::Tick;
677        let result = mgr.execute_program(&mut prog, &ctx, &MockEngine, 0.0, None);
678        // MockEngine.has_path returns true → WASM drops
679        let exec = result.unwrap();
680        let r = exec.hook_result.unwrap();
681        assert!(r.is_drop());
682    }
683
684    #[test]
685    fn host_has_path_null_engine() {
686        // NullEngine.has_path returns false → WASM continues
687        let mgr = make_manager();
688        let mut prog = mgr
689            .compile("pathcheck".into(), WAT_HOST_HAS_PATH.as_bytes(), 0)
690            .unwrap();
691        let ctx = HookContext::Tick;
692        let result = mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None);
693        let exec = result.unwrap();
694        let r = exec.hook_result.unwrap();
695        assert_eq!(r.verdict, Verdict::Continue as u32);
696    }
697
698    // --- New Phase 2 tests ---
699
700    /// Configurable mock engine for testing host functions.
701    struct MockEngineCustom {
702        announce_rate_val: Option<i32>,
703        link_state_val: Option<u8>,
704    }
705
706    impl EngineAccess for MockEngineCustom {
707        fn has_path(&self, _: &[u8; 16]) -> bool {
708            false
709        }
710        fn hops_to(&self, _: &[u8; 16]) -> Option<u8> {
711            None
712        }
713        fn next_hop(&self, _: &[u8; 16]) -> Option<[u8; 16]> {
714            None
715        }
716        fn is_blackholed(&self, _: &[u8; 16]) -> bool {
717            false
718        }
719        fn interface_name(&self, _: u64) -> Option<String> {
720            None
721        }
722        fn interface_mode(&self, _: u64) -> Option<u8> {
723            None
724        }
725        fn identity_hash(&self) -> Option<[u8; 16]> {
726            None
727        }
728        fn announce_rate(&self, _: u64) -> Option<i32> {
729            self.announce_rate_val
730        }
731        fn link_state(&self, _: &[u8; 16]) -> Option<u8> {
732            self.link_state_val
733        }
734    }
735
736    /// WAT: calls host_get_announce_rate(42), if result >= 0 → Drop, else Continue.
737    const WAT_ANNOUNCE_RATE: &str = r#"
738        (module
739            (import "env" "host_get_announce_rate" (func $get_rate (param i64) (result i32)))
740            (memory (export "memory") 1)
741            (func (export "__rns_abi_version") (result i32) (i32.const 1))
742            (func (export "on_hook") (param $ctx_ptr i32) (result i32)
743                (if (i32.ge_s (call $get_rate (i64.const 42)) (i32.const 0))
744                    (then
745                        (i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
746                    )
747                    (else
748                        (i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
749                    )
750                )
751                (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
752                (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
753                (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
754                (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
755                (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
756                (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
757                (i32.const 0x2000)
758            )
759        )
760    "#;
761
762    #[test]
763    fn host_get_announce_rate_found() {
764        // announce_rate returns Some(1500) (1.5 Hz * 1000) → Drop
765        let engine = MockEngineCustom {
766            announce_rate_val: Some(1500),
767            link_state_val: None,
768        };
769        let mgr = make_manager();
770        let mut prog = mgr
771            .compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0)
772            .unwrap();
773        let ctx = HookContext::Tick;
774        let exec = mgr
775            .execute_program(&mut prog, &ctx, &engine, 0.0, None)
776            .unwrap();
777        assert!(exec.hook_result.unwrap().is_drop());
778    }
779
780    #[test]
781    fn host_get_announce_rate_not_found() {
782        // announce_rate returns None → -1 → Continue
783        let engine = MockEngineCustom {
784            announce_rate_val: None,
785            link_state_val: None,
786        };
787        let mgr = make_manager();
788        let mut prog = mgr
789            .compile("rate".into(), WAT_ANNOUNCE_RATE.as_bytes(), 0)
790            .unwrap();
791        let ctx = HookContext::Tick;
792        let exec = mgr
793            .execute_program(&mut prog, &ctx, &engine, 0.0, None)
794            .unwrap();
795        assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
796    }
797
798    /// WAT: calls host_get_link_state with 16-byte hash at 0x3000.
799    /// If state == 2 (Active) → Drop, else Continue.
800    const WAT_LINK_STATE: &str = r#"
801        (module
802            (import "env" "host_get_link_state" (func $link_state (param i32) (result i32)))
803            (memory (export "memory") 1)
804            (func (export "__rns_abi_version") (result i32) (i32.const 1))
805            (func (export "on_hook") (param $ctx_ptr i32) (result i32)
806                (if (i32.eq (call $link_state (i32.const 0x3000)) (i32.const 2))
807                    (then
808                        (i32.store (i32.const 0x2000) (i32.const 1)) ;; Drop
809                    )
810                    (else
811                        (i32.store (i32.const 0x2000) (i32.const 0)) ;; Continue
812                    )
813                )
814                (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
815                (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
816                (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
817                (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
818                (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
819                (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
820                (i32.const 0x2000)
821            )
822        )
823    "#;
824
825    #[test]
826    fn host_get_link_state_active() {
827        // link_state returns Some(2) (Active) → Drop
828        let engine = MockEngineCustom {
829            announce_rate_val: None,
830            link_state_val: Some(2),
831        };
832        let mgr = make_manager();
833        let mut prog = mgr
834            .compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0)
835            .unwrap();
836        let ctx = HookContext::Tick;
837        let exec = mgr
838            .execute_program(&mut prog, &ctx, &engine, 0.0, None)
839            .unwrap();
840        assert!(exec.hook_result.unwrap().is_drop());
841    }
842
843    #[test]
844    fn host_get_link_state_not_found() {
845        // link_state returns None → -1, which != 2 → Continue
846        let engine = MockEngineCustom {
847            announce_rate_val: None,
848            link_state_val: None,
849        };
850        let mgr = make_manager();
851        let mut prog = mgr
852            .compile("linkst".into(), WAT_LINK_STATE.as_bytes(), 0)
853            .unwrap();
854        let ctx = HookContext::Tick;
855        let exec = mgr
856            .execute_program(&mut prog, &ctx, &engine, 0.0, None)
857            .unwrap();
858        assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
859    }
860
861    /// WAT: writes a SendOnInterface ActionWire at 0x3000 and calls host_inject_action.
862    /// Binary: tag=0 (SendOnInterface), interface=1 (u64 LE), data_offset=0x3100, data_len=4
863    /// Data at 0x3100: [0xDE, 0xAD, 0xBE, 0xEF]
864    /// Returns Continue.
865    const WAT_INJECT_ACTION: &str = r#"
866        (module
867            (import "env" "host_inject_action" (func $inject (param i32 i32) (result i32)))
868            (memory (export "memory") 1)
869            (func (export "__rns_abi_version") (result i32) (i32.const 1))
870            (func (export "on_hook") (param $ctx_ptr i32) (result i32)
871                ;; Write the data payload at 0x3100
872                (i32.store8 (i32.const 0x3100) (i32.const 0xDE))
873                (i32.store8 (i32.const 0x3101) (i32.const 0xAD))
874                (i32.store8 (i32.const 0x3102) (i32.const 0xBE))
875                (i32.store8 (i32.const 0x3103) (i32.const 0xEF))
876
877                ;; Write ActionWire at 0x3000:
878                ;; byte 0: tag = 0 (SendOnInterface)
879                (i32.store8 (i32.const 0x3000) (i32.const 0))
880                ;; bytes 1-8: interface = 1 (u64 LE)
881                (i64.store (i32.const 0x3001) (i64.const 1))
882                ;; bytes 9-12: data_offset = 0x3100 (u32 LE)
883                (i32.store (i32.const 0x3009) (i32.const 0x3100))
884                ;; bytes 13-16: data_len = 4 (u32 LE)
885                (i32.store (i32.const 0x300D) (i32.const 4))
886
887                ;; Call inject: ptr=0x3000, len=17 (1 + 8 + 4 + 4)
888                (drop (call $inject (i32.const 0x3000) (i32.const 17)))
889
890                ;; Return Continue
891                (i32.store (i32.const 0x2000) (i32.const 0))
892                (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
893                (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
894                (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
895                (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
896                (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
897                (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
898                (i32.const 0x2000)
899            )
900        )
901    "#;
902
903    #[test]
904    fn host_inject_action_send() {
905        let mgr = make_manager();
906        let mut prog = mgr
907            .compile("inject".into(), WAT_INJECT_ACTION.as_bytes(), 0)
908            .unwrap();
909        let ctx = HookContext::Tick;
910        let exec = mgr
911            .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
912            .unwrap();
913        assert_eq!(exec.hook_result.unwrap().verdict, Verdict::Continue as u32);
914        assert_eq!(exec.injected_actions.len(), 1);
915        match &exec.injected_actions[0] {
916            crate::wire::ActionWire::SendOnInterface { interface, raw } => {
917                assert_eq!(*interface, 1);
918                assert_eq!(raw, &[0xDE, 0xAD, 0xBE, 0xEF]);
919            }
920            other => panic!("expected SendOnInterface, got {:?}", other),
921        }
922    }
923
924    /// WAT: returns Modify (verdict=2) with modified data at 0x2100 (4 bytes).
925    const WAT_MODIFY: &str = r#"
926        (module
927            (memory (export "memory") 1)
928            (func (export "__rns_abi_version") (result i32) (i32.const 1))
929            (func (export "on_hook") (param $ctx_ptr i32) (result i32)
930                ;; Write modified data at 0x2100
931                (i32.store8 (i32.const 0x2100) (i32.const 0xAA))
932                (i32.store8 (i32.const 0x2101) (i32.const 0xBB))
933                (i32.store8 (i32.const 0x2102) (i32.const 0xCC))
934                (i32.store8 (i32.const 0x2103) (i32.const 0xDD))
935
936                ;; verdict = 2 (Modify)
937                (i32.store (i32.const 0x2000) (i32.const 2))
938                ;; modified_data_offset = 0x2100
939                (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x2100))
940                ;; modified_data_len = 4
941                (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
942                (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
943                (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
944                (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
945                (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
946                (i32.const 0x2000)
947            )
948        )
949    "#;
950
951    #[test]
952    fn modify_extracts_data() {
953        let mgr = make_manager();
954        let mut prog = mgr.compile("mod".into(), WAT_MODIFY.as_bytes(), 0).unwrap();
955        let ctx = HookContext::Tick;
956        let exec = mgr
957            .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
958            .unwrap();
959        let r = exec.hook_result.unwrap();
960        assert_eq!(r.verdict, Verdict::Modify as u32);
961        let data = exec.modified_data.unwrap();
962        assert_eq!(data, vec![0xAA, 0xBB, 0xCC, 0xDD]);
963    }
964
965    #[test]
966    fn chain_accumulates_injected_actions() {
967        // Chain: inject_action module (Continue) + Drop module
968        // Both should contribute to the result; injected actions should be accumulated
969        let mgr = make_manager();
970        let injector = mgr
971            .compile("injector".into(), WAT_INJECT_ACTION.as_bytes(), 100)
972            .unwrap();
973        let dropper = mgr
974            .compile("dropper".into(), WAT_DROP.as_bytes(), 0)
975            .unwrap();
976        let mut programs = vec![injector, dropper];
977        programs.sort_by(|a, b| b.priority.cmp(&a.priority));
978
979        let ctx = HookContext::Tick;
980        let exec = mgr
981            .run_chain(&mut programs, &ctx, &NullEngine, 0.0)
982            .unwrap();
983        // Chain should drop (second hook)
984        assert!(exec.hook_result.unwrap().is_drop());
985        // But injected action from first hook should be present
986        assert_eq!(exec.injected_actions.len(), 1);
987    }
988
989    // --- Instance persistence tests ---
990
991    /// WAT module with a mutable global counter. Each call increments it and
992    /// writes the counter value into the verdict field (abusing it as an integer).
993    /// This lets us verify that the global persists across calls.
994    const WAT_COUNTER: &str = r#"
995        (module
996            (memory (export "memory") 1)
997            (func (export "__rns_abi_version") (result i32) (i32.const 1))
998            (global $counter (mut i32) (i32.const 0))
999            (func (export "on_hook") (param i32) (result i32)
1000                ;; Increment counter
1001                (global.set $counter (i32.add (global.get $counter) (i32.const 1)))
1002                ;; Write counter value at 0x3000 (scratch area)
1003                (i32.store (i32.const 0x3000) (global.get $counter))
1004                ;; Return Continue with the counter stashed in modified_data region
1005                ;; verdict = 2 (Modify) so we can extract the counter via modified_data
1006                (i32.store (i32.const 0x2000) (i32.const 2))
1007                ;; modified_data_offset = 0x3000
1008                (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0x3000))
1009                ;; modified_data_len = 4
1010                (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 4))
1011                (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1012                (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1013                (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1014                (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1015                (i32.const 0x2000)
1016            )
1017        )
1018    "#;
1019
1020    fn extract_counter(exec: &ExecuteResult) -> u32 {
1021        let data = exec.modified_data.as_ref().expect("no modified data");
1022        assert_eq!(data.len(), 4);
1023        u32::from_le_bytes([data[0], data[1], data[2], data[3]])
1024    }
1025
1026    #[test]
1027    fn instance_persistence_counter() {
1028        let mgr = make_manager();
1029        let mut prog = mgr
1030            .compile("counter".into(), WAT_COUNTER.as_bytes(), 0)
1031            .unwrap();
1032        let ctx = HookContext::Tick;
1033
1034        // Call 3 times — counter should increment across calls
1035        let exec1 = mgr
1036            .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1037            .unwrap();
1038        assert_eq!(extract_counter(&exec1), 1);
1039
1040        let exec2 = mgr
1041            .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1042            .unwrap();
1043        assert_eq!(extract_counter(&exec2), 2);
1044
1045        let exec3 = mgr
1046            .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1047            .unwrap();
1048        assert_eq!(extract_counter(&exec3), 3);
1049    }
1050
1051    #[test]
1052    fn instance_persistence_resets_on_drop_cache() {
1053        let mgr = make_manager();
1054        let mut prog = mgr
1055            .compile("counter".into(), WAT_COUNTER.as_bytes(), 0)
1056            .unwrap();
1057        let ctx = HookContext::Tick;
1058
1059        // Increment twice
1060        mgr.execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1061            .unwrap();
1062        let exec2 = mgr
1063            .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1064            .unwrap();
1065        assert_eq!(extract_counter(&exec2), 2);
1066
1067        // Drop cache (simulates reload)
1068        prog.drop_cache();
1069
1070        // Counter should restart at 1
1071        let exec3 = mgr
1072            .execute_program(&mut prog, &ctx, &NullEngine, 0.0, None)
1073            .unwrap();
1074        assert_eq!(extract_counter(&exec3), 1);
1075    }
1076
1077    // --- ABI version validation tests ---
1078
1079    /// WAT module without __rns_abi_version export.
1080    const WAT_NO_ABI_VERSION: &str = r#"
1081        (module
1082            (memory (export "memory") 1)
1083            (func (export "on_hook") (param i32) (result i32)
1084                (i32.store (i32.const 0x2000) (i32.const 0))
1085                (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1086                (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1087                (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1088                (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1089                (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1090                (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1091                (i32.const 0x2000)
1092            )
1093        )
1094    "#;
1095
1096    /// WAT module with wrong ABI version (9999).
1097    const WAT_WRONG_ABI_VERSION: &str = r#"
1098        (module
1099            (memory (export "memory") 1)
1100            (func (export "__rns_abi_version") (result i32) (i32.const 9999))
1101            (func (export "on_hook") (param i32) (result i32)
1102                (i32.store (i32.const 0x2000) (i32.const 0))
1103                (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1104                (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1105                (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1106                (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1107                (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1108                (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1109                (i32.const 0x2000)
1110            )
1111        )
1112    "#;
1113
1114    #[test]
1115    fn rejects_missing_abi_version() {
1116        let mgr = make_manager();
1117        let result = mgr.compile("no_abi".into(), WAT_NO_ABI_VERSION.as_bytes(), 0);
1118        match result {
1119            Err(HookError::AbiVersionMismatch {
1120                hook_name,
1121                expected,
1122                found,
1123            }) => {
1124                assert_eq!(hook_name, "no_abi");
1125                assert_eq!(expected, HOST_ABI_VERSION);
1126                assert_eq!(found, None);
1127            }
1128            other => panic!(
1129                "expected AbiVersionMismatch with found=None, got {:?}",
1130                other.err()
1131            ),
1132        }
1133    }
1134
1135    #[test]
1136    fn rejects_wrong_abi_version() {
1137        let mgr = make_manager();
1138        let result = mgr.compile("bad_abi".into(), WAT_WRONG_ABI_VERSION.as_bytes(), 0);
1139        match result {
1140            Err(HookError::AbiVersionMismatch {
1141                hook_name,
1142                expected,
1143                found,
1144            }) => {
1145                assert_eq!(hook_name, "bad_abi");
1146                assert_eq!(expected, HOST_ABI_VERSION);
1147                assert_eq!(found, Some(9999));
1148            }
1149            other => panic!(
1150                "expected AbiVersionMismatch with found=Some(9999), got {:?}",
1151                other.err()
1152            ),
1153        }
1154    }
1155
1156    #[test]
1157    fn accepts_correct_abi_version() {
1158        let mgr = make_manager();
1159        let result = mgr.compile("good_abi".into(), WAT_CONTINUE.as_bytes(), 0);
1160        assert!(
1161            result.is_ok(),
1162            "compile should succeed with correct ABI version"
1163        );
1164    }
1165
1166    #[test]
1167    fn host_emit_event_collects_provider_event() {
1168        let mgr = make_manager();
1169        let wat = r#"
1170            (module
1171                (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1172                (memory (export "memory") 1)
1173                (data (i32.const 0x3000) "packet")
1174                (data (i32.const 0x3010) "\01\02\03")
1175                (func (export "__rns_abi_version") (result i32) (i32.const 1))
1176                (func (export "on_hook") (param i32) (result i32)
1177                    (drop (call $emit (i32.const 0x3000) (i32.const 6) (i32.const 0x3010) (i32.const 3)))
1178                    (i32.store (i32.const 0x2000) (i32.const 0))
1179                    (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1180                    (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1181                    (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1182                    (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1183                    (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1184                    (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1185                    (i32.const 0x2000)
1186                )
1187            )
1188        "#;
1189        let module = mgr.runtime.compile(&wat::parse_str(wat).unwrap()).unwrap();
1190        let mut prog = LoadedProgram::new("emit".into(), module, 0);
1191        let ctx = HookContext::Tick;
1192
1193        let exec = mgr
1194            .execute_program_with_provider_events(&mut prog, &ctx, &NullEngine, 0.0, true, None)
1195            .unwrap();
1196        assert_eq!(exec.provider_events.len(), 1);
1197        assert_eq!(exec.provider_events[0].hook_name, "emit");
1198        assert_eq!(exec.provider_events[0].payload_type, "packet");
1199        assert_eq!(exec.provider_events[0].payload, vec![1, 2, 3]);
1200    }
1201
1202    #[test]
1203    fn host_emit_event_is_ignored_when_disabled() {
1204        let mgr = make_manager();
1205        let wat = r#"
1206            (module
1207                (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1208                (memory (export "memory") 1)
1209                (data (i32.const 0x3000) "packet")
1210                (data (i32.const 0x3010) "\01\02\03")
1211                (func (export "__rns_abi_version") (result i32) (i32.const 1))
1212                (func (export "on_hook") (param i32) (result i32)
1213                    (drop (call $emit (i32.const 0x3000) (i32.const 6) (i32.const 0x3010) (i32.const 3)))
1214                    (i32.store (i32.const 0x2000) (i32.const 0))
1215                    (i32.store (i32.add (i32.const 0x2000) (i32.const 4)) (i32.const 0))
1216                    (i32.store (i32.add (i32.const 0x2000) (i32.const 8)) (i32.const 0))
1217                    (i32.store (i32.add (i32.const 0x2000) (i32.const 12)) (i32.const 0))
1218                    (i32.store (i32.add (i32.const 0x2000) (i32.const 16)) (i32.const 0))
1219                    (i32.store (i32.add (i32.const 0x2000) (i32.const 20)) (i32.const 0))
1220                    (i32.store (i32.add (i32.const 0x2000) (i32.const 24)) (i32.const 0))
1221                    (i32.const 0x2000)
1222                )
1223            )
1224        "#;
1225        let module = mgr.runtime.compile(&wat::parse_str(wat).unwrap()).unwrap();
1226        let mut prog = LoadedProgram::new("emit".into(), module, 0);
1227        let ctx = HookContext::Tick;
1228
1229        let exec = mgr
1230            .execute_program_with_provider_events(&mut prog, &ctx, &NullEngine, 0.0, false, None)
1231            .unwrap();
1232        assert!(exec.provider_events.is_empty());
1233    }
1234
1235    #[test]
1236    fn run_chain_returns_continue_only_provider_events() {
1237        let manager = make_manager();
1238        let wasm = wat::parse_str(
1239            r#"(module
1240                (import "env" "host_emit_event" (func $emit (param i32 i32 i32 i32) (result i32)))
1241                (memory (export "memory") 1)
1242                (data (i32.const 4096) "\00\00\00\00")
1243                (data (i32.const 8192) "packet")
1244                (data (i32.const 8208) "\01\02\03")
1245                (func (export "__rns_abi_version") (result i32) i32.const 1)
1246                (func (export "on_hook") (param i32) (result i32)
1247                    i32.const 8192
1248                    i32.const 6
1249                    i32.const 8208
1250                    i32.const 3
1251                    call $emit
1252                    drop
1253                    i32.const 4096
1254                )
1255            )"#,
1256        )
1257        .unwrap();
1258
1259        let mut programs = vec![manager.compile("emit".into(), &wasm, 0).unwrap()];
1260        let pkt_ctx = crate::PacketContext {
1261            flags: 0,
1262            hops: 0,
1263            destination_hash: [0; 16],
1264            context: 0,
1265            packet_hash: [0; 32],
1266            interface_id: 1,
1267            data_offset: 0,
1268            data_len: 0,
1269        };
1270        let ctx = crate::HookContext::Packet {
1271            ctx: &pkt_ctx,
1272            raw: &[],
1273        };
1274
1275        let exec = manager
1276            .run_chain_with_provider_events(&mut programs, &ctx, &NullEngine, 0.0, true)
1277            .expect("expected provider event result");
1278        assert!(exec.hook_result.is_none());
1279        assert!(exec.injected_actions.is_empty());
1280        assert_eq!(exec.provider_events.len(), 1);
1281        assert_eq!(exec.provider_events[0].payload_type, "packet");
1282        assert_eq!(exec.provider_events[0].payload, vec![1, 2, 3]);
1283    }
1284}