Skip to main content

hopper_core/event/
mod.rs

1//! Zero-allocation event emission via `sol_log_data`.
2//!
3//! Events are emitted as raw Pod bytes through the `sol_log_data` syscall.
4//! This is ~100 CU and zero-allocation. For unforgeable events, use
5//! `emit_event_cpi` which self-invokes the current program (~1000 CU).
6//!
7//! ## Dual emission model
8//!
9//! | Function | CU Cost | Spoofable? | Use case |
10//! |---|---|---|---|
11//! | `emit_event` | ~100 | Yes (any program can log) | Fast indexer events |
12//! | `emit_event_cpi` | ~1000 | No (verified via self-CPI) | Trustworthy audit trail |
13
14use crate::account::{FixedLayout, Pod};
15use hopper_runtime::error::ProgramError;
16
17/// Emit a Pod event via `sol_log_data`.
18///
19/// The event is logged as raw bytes. Clients decode using the schema manifest
20/// or known layout. Costs ~100 CU, zero allocation.
21#[inline(always)]
22pub fn emit_event<T: Pod + FixedLayout>(value: &T) -> Result<(), ProgramError> {
23    // SAFETY: T: Pod guarantees all bit patterns valid and no padding invariants.
24    // The resulting slice covers exactly T::SIZE bytes from a valid reference.
25    let bytes = unsafe { core::slice::from_raw_parts(value as *const T as *const u8, T::SIZE) };
26    emit_slices(&[bytes]);
27    Ok(())
28}
29
30/// Emit event with a discriminator prefix for easy client-side filtering.
31///
32/// Layout: `[event_disc: u8][event_data: T::SIZE bytes]`
33#[inline]
34pub fn emit_event_tagged<T: Pod + FixedLayout>(disc: u8, value: &T) -> Result<(), ProgramError> {
35    // SAFETY: T: Pod guarantees all bit patterns valid. Slice covers T::SIZE bytes.
36    let value_bytes =
37        unsafe { core::slice::from_raw_parts(value as *const T as *const u8, T::SIZE) };
38    let disc_bytes = [disc];
39    emit_slices(&[&disc_bytes[..], value_bytes]);
40    Ok(())
41}
42
43/// Emit one or more byte slices as a single `sol_log_data` entry.
44#[inline(always)]
45pub fn emit_slices(segments: &[&[u8]]) {
46    #[cfg(target_os = "solana")]
47    {
48        // SAFETY: segments is a valid slice of (ptr, len) pairs as expected
49        // by the sol_log_data syscall. BPF ABI guarantees layout compatibility.
50        unsafe {
51            hopper_runtime::syscalls::sol_log_data(
52                segments.as_ptr() as *const u8,
53                segments.len() as u64,
54            );
55        }
56    }
57    #[cfg(not(target_os = "solana"))]
58    {
59        let _ = segments;
60    }
61}
62
63/// Emit an unforgeable event via self-CPI (~1000 CU).
64///
65/// This self-invokes the current program with special event data, so
66/// the event appears in the transaction as a CPI from the program itself.
67/// Indexers can verify the event origin matches the program ID, making
68/// spoofing impossible.
69///
70/// Event CPI data layout: `[0xFF, 0xFE][event_disc: u8][event_data: T::SIZE bytes]`
71///
72/// The `0xFF 0xFE` prefix is an invalid instruction discriminator reserved
73/// for event CPI, the program's dispatch will never match it.
74#[cfg(feature = "cpi")]
75#[inline]
76pub fn emit_event_cpi<T: Pod + FixedLayout>(
77    disc: u8,
78    value: &T,
79    program_id: &hopper_runtime::Address,
80    accounts: &[&hopper_runtime::AccountView],
81) -> Result<(), ProgramError> {
82    // Build event data: [0xFF, 0xFE, disc, ...value_bytes]
83    // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
84    let value_bytes =
85        unsafe { core::slice::from_raw_parts(value as *const T as *const u8, T::SIZE) };
86
87    // Emit via sol_log_data first (cheap, for indexers).
88    let disc_byte = [disc];
89    emit_slices(&[&disc_byte[..], value_bytes]);
90
91    // Self-CPI for unforgeable proof.
92    #[cfg(target_os = "solana")]
93    {
94        const EVENT_CPI_PREFIX: [u8; 2] = [0xFF, 0xFE];
95
96        use hopper_runtime::instruction::{InstructionAccount, InstructionView};
97
98        // Build instruction data on stack: prefix + disc + value bytes.
99        // Max event data = 1024 - 3 = 1021 bytes.
100        let data_len = 3 + T::SIZE;
101        if data_len > 1024 {
102            return Err(ProgramError::InvalidArgument);
103        }
104        let mut data_buf = [0u8; 1024];
105        data_buf[0] = EVENT_CPI_PREFIX[0];
106        data_buf[1] = EVENT_CPI_PREFIX[1];
107        data_buf[2] = disc;
108        // SAFETY: value_bytes length = T::SIZE, verified via Pod + FixedLayout.
109        unsafe {
110            core::ptr::copy_nonoverlapping(
111                value_bytes.as_ptr(),
112                data_buf.as_mut_ptr().add(3),
113                T::SIZE,
114            );
115        }
116
117        let empty_accounts: [InstructionAccount; 0] = [];
118        let instruction = InstructionView {
119            program_id,
120            data: &data_buf[..data_len],
121            accounts: &empty_accounts,
122        };
123
124        // Build CPI accounts from the provided account views.
125        let mut cpi_accounts_buf: [core::mem::MaybeUninit<hopper_runtime::instruction::CpiAccount>; 32] =
126            // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
127            unsafe { core::mem::MaybeUninit::uninit().assume_init() };
128        let count = accounts.len().min(32);
129        let mut i = 0;
130        while i < count {
131            cpi_accounts_buf[i] = core::mem::MaybeUninit::new(
132                hopper_runtime::instruction::CpiAccount::from(accounts[i]),
133            );
134            i += 1;
135        }
136        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
137        let cpi_accounts = unsafe {
138            core::slice::from_raw_parts(
139                cpi_accounts_buf.as_ptr() as *const hopper_runtime::instruction::CpiAccount,
140                count,
141            )
142        };
143
144        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
145        unsafe {
146            hopper_runtime::cpi::invoke_unchecked(&instruction, cpi_accounts)?;
147        }
148    }
149    #[cfg(not(target_os = "solana"))]
150    {
151        let _ = (program_id, accounts);
152    }
153
154    Ok(())
155}