Skip to main content

hopper_runtime/
cpi_event.rs

1//! Self-CPI event emission: the wire format, the verification
2//! primitives, and the runtime half of the one-line macro surface.
3//!
4//! Log output is lossy. Transaction metadata is not. A program that
5//! needs events to arrive at indexers regardless of log truncation
6//! invokes itself with a distinctive CPI whose bytes carry the event
7//! payload. This serves the same persistence role as Anchor's `emit_cpi!`.
8//!
9//! ## The one-liner
10//!
11//! Programs normally use the macro-generated context surface:
12//!
13//! ```ignore
14//! #[hopper::context(event_cpi)]        // ← one attribute option
15//! pub struct Deposit {
16//!     #[account(mut)]
17//!     pub vault: Vault,
18//! }
19//!
20//! #[hopper::program]
21//! mod vault_prog {
22//!     #[instruction(0)]
23//!     fn deposit(ctx: Context<Deposit>, amount: u64) -> ProgramResult {
24//!         // ... state changes ...
25//!         ctx.emit_event_cpi(&Deposited { amount: WireU64::new(amount) })?;  // ← one call
26//!         Ok(())
27//!     }
28//! }
29//! ```
30//!
31//! `event_cpi` appends two trailing accounts to the context (the
32//! event-authority PDA and the program account itself, the same two
33//! Anchor's `#[event_cpi]` appends), validates them at bind, exposes
34//! `ctx.emit_event_cpi(&event)` on the bound context, and the
35//! `#[hopper::program]` dispatcher grows a self-CPI sink on the
36//! reserved `[0xE0, 0x1E]` marker that authenticates each event before
37//! accepting it. The macro emits the sink path only for contexts that opt in.
38//!
39//! ## Wire format
40//!
41//! ```text
42//! [0..2]   CPI_EVENT_MARKER   (0xE0, 0x1E)
43//! [2]      event tag          (the byte from `#[hopper::event(tag = N)]`)
44//! [3..]    event payload      (the event's Pod bytes)
45//! ```
46//!
47//! Three bytes of instruction-data overhead per event. Anchor's
48//! `emit_cpi!` spends sixteen: the 8-byte `EVENT_IX_TAG_LE` instruction
49//! discriminator plus the event's own 8-byte account-style
50//! discriminator. Hopper's 2-byte marker + 1-byte tag table carries the
51//! same routing information for 13 fewer instruction-data bytes per
52//! event (5 fewer counting only the event-identification layer: 3-byte
53//! marker+tag vs one 8-byte hash discriminator).
54//!
55//! ## Why the sink verifies
56//!
57//! The generated sink is not a bare no-op. Any program can CPI into
58//! any other program, so an unauthenticated sink would let an attacker
59//! program plant forged "events" in your program's inner-instruction
60//! list. The sink therefore requires the event-authority PDA, derived with
61//! [`EVENT_AUTHORITY_SEED`] under this program's id, to sign the
62//! CPI. Only this program's own `invoke_signed` can produce that
63//! signature, which is exactly Anchor's authenticity argument for its
64//! `event_authority` account.
65//!
66//! On-chain verification uses [`crate::pda::find_and_verify_pda`]. Anchor
67//! v0.31+ pins the same PDA against a compile-time
68//! constant; Hopper has no compile-time program id, so it derives at
69//! runtime. Off-chain hosts have no sha256
70//! syscall (see [`crate::pda`]), so host builds enforce the marker and
71//! the signer flag and document the address pin as an on-chain check.
72//!
73//! ## Manual wiring (appendix)
74//!
75//! The pre-`event_cpi` manual pattern remains supported for programs
76//! that want custom control. Declare the sentinel yourself:
77//!
78//! ```ignore
79//! #[instruction(discriminator = [0xE0, 0x1E])]
80//! fn __hopper_event_sink(ctx: &mut Context<'_>) -> ProgramResult {
81//!     // Recommended: authenticate instead of no-op'ing.
82//!     hopper_runtime::cpi_event::handle_event_sink(ctx, ctx.instruction_data())
83//! }
84//! ```
85//!
86//! pass the event-authority PDA + program account in your context, and
87//! emit through [`crate::hopper_emit_cpi!`] (or [`encode_event_cpi`] +
88//! [`invoke_event_cpi`] for full control).
89
90/// The reserved self-CPI event discriminator.
91///
92/// Placed at the start of every emitted event CPI. The generated
93/// dispatcher routes instruction data with this prefix to the event
94/// sink; manual programs match it with
95/// `#[instruction(discriminator = [0xE0, 0x1E])]`.
96pub const CPI_EVENT_MARKER: [u8; 2] = [0xE0, 0x1E];
97
98/// Canonical PDA seed for the Hopper event-authority. The
99/// `#[hopper::context(event_cpi)]` machinery derives, verifies, and
100/// signs with this seed; manual programs must match it so the CPI
101/// signer resolves.
102pub const EVENT_AUTHORITY_SEED: &[u8] = b"__hopper_event_authority";
103
104/// Maximum event payload accepted by the emit helpers' stack buffer.
105///
106/// `3 + MAX_EVENT_PAYLOAD` bytes of stack per emit call. 512 payload
107/// bytes fits every sensibly-sized event; larger events should use the
108/// log-based `emit!` path or hand-rolled encoding.
109pub const MAX_EVENT_PAYLOAD: usize = 512;
110
111/// The bump byte host builds report for the event authority.
112///
113/// Off-chain targets have no sha256 syscall, so the real bump cannot be
114/// derived there (see [`crate::pda::find_program_address`]). Host-side
115/// `bind()` therefore records this placeholder; the host CPI emulation
116/// validates the signer *dimension* (the fixture must be marked signer)
117/// rather than the derivation. On-chain the recorded bump is always the
118/// real one returned by the sha256 verify loop.
119pub const HOST_EVENT_AUTHORITY_BUMP: u8 = 255;
120
121/// A self-CPI-emittable event: a stable 1-byte tag plus a borrowed
122/// payload view.
123///
124/// `#[hopper::event]` implements this automatically (the tag is the
125/// `tag = N` byte, the payload is the struct's Pod bytes), which is
126/// what lets `ctx.emit_event_cpi(&event)` and
127/// [`crate::hopper_emit_cpi!`] accept any declared event without
128/// hand-written glue.
129pub trait CpiEvent {
130    /// Stable event discriminator tag byte (`#[hopper::event(tag = N)]`).
131    const TAG: u8;
132
133    /// Borrowed byte view of the event payload (no marker, no tag).
134    fn payload_bytes(&self) -> &[u8];
135
136    /// Value-level accessor for [`Self::TAG`], for macro call sites
137    /// that only hold an expression.
138    #[inline(always)]
139    fn tag(&self) -> u8 {
140        Self::TAG
141    }
142}
143
144/// Fill an out buffer with the CPI wire format for an event.
145///
146/// Returns the number of bytes written. Caller picks the buffer size;
147/// `2 + 1 + payload.len()` is always sufficient. Returns `None` if
148/// the out buffer is too small.
149///
150/// Zero-alloc. Compiles to a pair of `copy_from_slice` calls.
151///
152/// ```ignore
153/// let mut buf = [0u8; 3 + Deposited::PACKED_SIZE];
154/// let len = hopper_runtime::cpi_event::encode_event_cpi(
155///     Deposited::TAG,
156///     event.payload_bytes(),
157///     &mut buf,
158/// ).unwrap();
159/// ```
160#[inline]
161pub fn encode_event_cpi(event_tag: u8, event_payload: &[u8], out: &mut [u8]) -> Option<usize> {
162    let total = 2 + 1 + event_payload.len();
163    if out.len() < total {
164        return None;
165    }
166    out[0..2].copy_from_slice(&CPI_EVENT_MARKER);
167    out[2] = event_tag;
168    out[3..total].copy_from_slice(event_payload);
169    Some(total)
170}
171
172/// Decode the CPI wire format back into `(tag, payload)`.
173///
174/// The exact inverse of [`encode_event_cpi`]: returns `None` unless the
175/// data starts with [`CPI_EVENT_MARKER`] and carries at least the tag
176/// byte. Indexers scanning inner instructions and tests asserting
177/// round-trips both use this as the single source of decode truth.
178#[inline]
179pub fn decode_event_cpi(data: &[u8]) -> Option<(u8, &[u8])> {
180    if data.len() < 3 || data[0..2] != CPI_EVENT_MARKER {
181        return None;
182    }
183    Some((data[2], &data[3..]))
184}
185
186/// Verify an account is this program's event-authority PDA and return
187/// its bump.
188///
189/// This is the bind-time check behind `#[hopper::context(event_cpi)]`:
190/// the appended `event_authority` account must be the PDA of
191/// [`EVENT_AUTHORITY_SEED`] under the executing program id. On-chain it
192/// runs the sha256-only verify loop (`find_and_verify_pda`, ~200 CU for
193/// bump 255) and returns the real bump for the CPI signer seeds.
194///
195/// Off-chain hosts cannot derive (no sha256 syscall; see
196/// [`crate::pda::find_program_address`]), so the host branch accepts
197/// the account and reports [`HOST_EVENT_AUTHORITY_BUMP`]; the host CPI
198/// emulation still enforces the signer dimension at emit time.
199#[inline]
200pub fn verify_event_authority(
201    event_authority: &crate::account::AccountView<'_>,
202    program_id: &crate::address::Address,
203) -> Result<u8, crate::error::ProgramError> {
204    #[cfg(target_os = "solana")]
205    {
206        crate::pda::find_and_verify_pda(event_authority, &[EVENT_AUTHORITY_SEED], program_id)
207    }
208    #[cfg(not(target_os = "solana"))]
209    {
210        let _ = (event_authority, program_id);
211        Ok(HOST_EVENT_AUTHORITY_BUMP)
212    }
213}
214
215/// The event sink: validate an incoming self-CPI event instruction.
216///
217/// The `#[hopper::program]` dispatcher routes instruction data whose
218/// first bytes match [`CPI_EVENT_MARKER`] here (for programs whose
219/// contexts opted into `event_cpi`). Checks, in order:
220///
221/// 1. the data really is `[0xE0, 0x1E, tag, ..]` (≥ 3 bytes);
222/// 2. `accounts[0]`, the event authority, signed the CPI. Only this
223///    program's own `invoke_signed` can sign for its event-authority
224///    PDA, so this is what makes accepted events authentic;
225/// 3. (on-chain) `accounts[0]`'s address is the PDA of
226///    [`EVENT_AUTHORITY_SEED`] under this program id, via the sha256
227///    verify loop. Without the address pin, any keypair the attacker
228///    controls could satisfy the signer check. Hosts have no sha256
229///    syscall, so off-chain builds stop at the signer check.
230///
231/// Returns `Ok(())` for a valid event, which is all a sink must do:
232/// the payload lives in the transaction's inner-instruction record.
233#[inline]
234pub fn handle_event_sink(
235    ctx: &crate::context::Context<'_>,
236    data: &[u8],
237) -> crate::result::ProgramResult {
238    if data.len() < 3 || data[0..2] != CPI_EVENT_MARKER {
239        return Err(crate::error::ProgramError::InvalidInstructionData);
240    }
241    let authority = ctx.account(0)?;
242    if !authority.is_signer() {
243        return Err(crate::error::ProgramError::MissingRequiredSignature);
244    }
245    #[cfg(target_os = "solana")]
246    {
247        let _bump =
248            crate::pda::find_and_verify_pda(authority, &[EVENT_AUTHORITY_SEED], ctx.program_id())?;
249    }
250    Ok(())
251}
252
253/// Invoke a self-CPI carrying the encoded event payload.
254///
255/// Builds the one-account instruction (event-authority as signer) and
256/// hands it to Hopper's checked `invoke_signed`, so the emit rides the
257/// cheapest safe invoke tier: on-chain that is the fused
258/// validate+build pass over one account followed by the CPI syscall;
259/// off-chain the same call runs address/writability/borrow validation and
260/// verifies that PDA authority was supplied, then records the would-be inner
261/// instruction for test
262/// harnesses (under `test` or the `thread-local-registry` feature) so
263/// end-to-end tests can assert the exact wire bytes.
264///
265/// This is the function `ctx.emit_event_cpi(..)` and
266/// [`crate::hopper_emit_cpi!`] call. Users who want finer-grained
267/// control over the CPI (extra accounts, custom signer) can call this
268/// directly with their own encoded data.
269#[inline]
270pub fn invoke_event_cpi(
271    program_id: &crate::address::Address,
272    event_authority: &crate::account::AccountView<'_>,
273    data: &[u8],
274    authority_seeds: &[&[u8]],
275) -> crate::result::ProgramResult {
276    use crate::instruction::{InstructionAccount, InstructionView, Seed, Signer};
277    if authority_seeds.len() > crate::address::MAX_SEEDS {
278        return Err(crate::error::ProgramError::MaxSeedLengthExceeded);
279    }
280
281    let account_meta = InstructionAccount {
282        address: event_authority.address(),
283        is_signer: true,
284        is_writable: false,
285    };
286    let ix = InstructionView {
287        program_id,
288        accounts: ::core::slice::from_ref(&account_meta),
289        data,
290    };
291    let mut seed_storage: [::core::mem::MaybeUninit<Seed<'_>>; crate::address::MAX_SEEDS] =
292        // SAFETY: MaybeUninit elements do not require initialization.
293        unsafe { ::core::mem::MaybeUninit::uninit().assume_init() };
294    let mut seed_index = 0;
295    while seed_index < authority_seeds.len() {
296        seed_storage[seed_index].write(Seed::from(authority_seeds[seed_index]));
297        seed_index += 1;
298    }
299    let seed_slice =
300        // SAFETY: The first `authority_seeds.len()` slots were initialized above.
301        unsafe {
302            ::core::slice::from_raw_parts(
303                seed_storage.as_ptr() as *const Seed<'_>,
304                authority_seeds.len(),
305            )
306        };
307    let signer_list = [Signer::from(seed_slice)];
308    let account_views = [event_authority];
309    crate::cpi::invoke_signed::<1>(&ix, &account_views, &signer_list)?;
310
311    // Host observation point: after the emulated CPI validates, record
312    // the inner instruction a real transaction would carry, so host
313    // test harnesses can assert the exact marker+tag+payload bytes.
314    // Compiled out entirely on-chain and on hosts without the test cfg.
315    #[cfg(all(
316        not(target_os = "solana"),
317        any(test, feature = "thread-local-registry")
318    ))]
319    host_capture::record(program_id, event_authority.address(), data);
320
321    Ok(())
322}
323
324/// Host-side capture of emitted event CPIs, for test observation.
325///
326/// On-chain the self-CPI lands in the transaction's inner-instruction
327/// metadata; off-chain there is no ledger, so [`invoke_event_cpi`]
328/// records each successful emit here instead. Per-thread (the same
329/// invocation-scope reasoning as the borrow registry's
330/// `thread-local-registry` lane), available under `test` or the
331/// `thread-local-registry` feature, exactly the lanes where `std` is
332/// already linked.
333#[cfg(all(
334    not(target_os = "solana"),
335    any(test, feature = "thread-local-registry")
336))]
337mod host_capture {
338    use core::cell::RefCell;
339
340    /// One captured self-CPI event emission.
341    #[derive(Clone, Debug, PartialEq, Eq)]
342    pub struct CapturedEventCpi {
343        /// The program that emitted (and is the CPI target).
344        pub program_id: crate::address::Address,
345        /// The event-authority account passed as the CPI signer.
346        pub authority: crate::address::Address,
347        /// The exact instruction data: marker + tag + payload.
348        pub data: std::vec::Vec<u8>,
349    }
350
351    std::thread_local! {
352        static CAPTURED: RefCell<std::vec::Vec<CapturedEventCpi>> =
353            const { RefCell::new(std::vec::Vec::new()) };
354    }
355
356    pub(super) fn record(
357        program_id: &crate::address::Address,
358        authority: &crate::address::Address,
359        data: &[u8],
360    ) {
361        CAPTURED.with(|captured| {
362            captured.borrow_mut().push(CapturedEventCpi {
363                program_id: *program_id,
364                authority: *authority,
365                data: data.to_vec(),
366            });
367        });
368    }
369
370    /// Drain this thread's captured event CPIs (oldest first).
371    pub fn take_host_captured_event_cpis() -> std::vec::Vec<CapturedEventCpi> {
372        CAPTURED.with(|captured| core::mem::take(&mut *captured.borrow_mut()))
373    }
374}
375
376#[cfg(all(
377    not(target_os = "solana"),
378    any(test, feature = "thread-local-registry")
379))]
380pub use host_capture::{take_host_captured_event_cpis, CapturedEventCpi};
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385    use crate::account::AccountView;
386    use crate::address::Address;
387    use crate::context::Context;
388    use crate::error::ProgramError;
389    use hopper_native::{
390        AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
391    };
392
393    #[test]
394    fn encodes_marker_tag_and_payload_in_order() {
395        let mut buf = [0u8; 16];
396        let len = encode_event_cpi(0x42, &[1, 2, 3, 4], &mut buf).unwrap();
397        assert_eq!(len, 7);
398        assert_eq!(&buf[..len], &[0xE0, 0x1E, 0x42, 1, 2, 3, 4]);
399    }
400
401    #[test]
402    fn rejects_short_buffer() {
403        let mut buf = [0u8; 3];
404        let len = encode_event_cpi(0, &[1, 2, 3, 4], &mut buf);
405        assert!(len.is_none());
406    }
407
408    #[test]
409    fn zero_payload_is_valid() {
410        let mut buf = [0u8; 3];
411        let len = encode_event_cpi(0x7F, &[], &mut buf).unwrap();
412        assert_eq!(len, 3);
413        assert_eq!(&buf[..len], &[0xE0, 0x1E, 0x7F]);
414    }
415
416    #[test]
417    fn reserved_marker_is_stable() {
418        assert_eq!(CPI_EVENT_MARKER, [0xE0, 0x1E]);
419    }
420
421    #[test]
422    fn decode_is_the_exact_inverse_of_encode() {
423        let payload = [9u8, 8, 7, 6, 5];
424        let mut buf = [0u8; 3 + 5];
425        let len = encode_event_cpi(0x2A, &payload, &mut buf).unwrap();
426        let (tag, decoded) = decode_event_cpi(&buf[..len]).expect("decodable");
427        assert_eq!(tag, 0x2A);
428        assert_eq!(decoded, &payload);
429
430        // Zero payload round-trips too.
431        let mut buf3 = [0u8; 3];
432        let len3 = encode_event_cpi(0x01, &[], &mut buf3).unwrap();
433        assert_eq!(decode_event_cpi(&buf3[..len3]), Some((0x01, &[][..])));
434    }
435
436    #[test]
437    fn decode_rejects_short_or_mismarked_data() {
438        assert_eq!(decode_event_cpi(&[]), None);
439        assert_eq!(decode_event_cpi(&[0xE0, 0x1E]), None, "marker without tag");
440        assert_eq!(decode_event_cpi(&[0xE0, 0x77, 0x01]), None, "wrong marker");
441        assert_eq!(decode_event_cpi(&[0x00, 0x1E, 0x01]), None, "wrong marker");
442    }
443
444    #[test]
445    fn trait_tag_defaults_to_the_associated_const() {
446        struct Ping;
447        impl CpiEvent for Ping {
448            const TAG: u8 = 0x5A;
449            fn payload_bytes(&self) -> &[u8] {
450                &[]
451            }
452        }
453        assert_eq!(Ping.tag(), 0x5A);
454    }
455
456    /// Build a minimal host account fixture (same pattern as the
457    /// context write-policy tests).
458    fn make_account(
459        address_byte: u8,
460        is_signer: bool,
461    ) -> (std::vec::Vec<u64>, AccountView<'static>) {
462        const DATA_LEN: usize = 8;
463        let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + DATA_LEN).div_ceil(8)];
464        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
465        // SAFETY: `backing` is sized for the header plus DATA_LEN bytes and
466        // outlives the returned view (the caller holds the Vec).
467        unsafe {
468            raw.write(RuntimeAccount {
469                borrow_state: NOT_BORROWED,
470                is_signer: is_signer as u8,
471                is_writable: 0,
472                executable: 0,
473                resize_delta: 0,
474                address: NativeAddress::new_from_array([address_byte; 32]),
475                owner: NativeAddress::new_from_array([0; 32]),
476                lamports: 0,
477                data_len: DATA_LEN as u64,
478            });
479        }
480        // SAFETY: `raw` points at a fully initialized RuntimeAccount with
481        // its data region in the same allocation.
482        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
483        (backing, AccountView::from_backend(backend))
484    }
485
486    #[test]
487    fn sink_rejects_short_data_and_wrong_marker() {
488        let (_b, authority) = make_account(1, true);
489        let accounts = [authority];
490        let pid = Address::new([9u8; 32]);
491        let ctx = Context::new(&pid, &accounts, &[]);
492
493        assert_eq!(
494            handle_event_sink(&ctx, &[0xE0, 0x1E]),
495            Err(ProgramError::InvalidInstructionData),
496            "marker without a tag byte must be refused"
497        );
498        assert_eq!(
499            handle_event_sink(&ctx, &[0xE0, 0x77, 0x01]),
500            Err(ProgramError::InvalidInstructionData),
501            "a wrong second marker byte must be refused"
502        );
503    }
504
505    #[test]
506    fn sink_requires_the_authority_to_sign() {
507        let (_b, authority) = make_account(1, false);
508        let accounts = [authority];
509        let pid = Address::new([9u8; 32]);
510        let ctx = Context::new(&pid, &accounts, &[]);
511
512        assert_eq!(
513            handle_event_sink(&ctx, &[0xE0, 0x1E, 0x42, 1, 2]),
514            Err(ProgramError::MissingRequiredSignature),
515            "an unsigned authority is a forged event and must be refused"
516        );
517    }
518
519    #[test]
520    fn sink_accepts_a_signed_authority_on_host() {
521        // Host builds stop at the signer check (no sha256 syscall to pin
522        // the PDA address); the address pin is on-chain-only, documented
523        // on `handle_event_sink`.
524        let (_b, authority) = make_account(1, true);
525        let accounts = [authority];
526        let pid = Address::new([9u8; 32]);
527        let ctx = Context::new(&pid, &accounts, &[]);
528
529        assert_eq!(handle_event_sink(&ctx, &[0xE0, 0x1E, 0x42]), Ok(()));
530    }
531
532    #[test]
533    fn sink_requires_the_authority_account_to_be_present() {
534        let pid = Address::new([9u8; 32]);
535        let accounts: [AccountView<'static>; 0] = [];
536        let ctx = Context::new(&pid, &accounts, &[]);
537        assert!(
538            handle_event_sink(&ctx, &[0xE0, 0x1E, 0x42]).is_err(),
539            "a sink CPI without the authority account must be refused"
540        );
541    }
542
543    #[test]
544    fn host_verify_event_authority_reports_the_placeholder_bump() {
545        let (_b, authority) = make_account(3, false);
546        let pid = Address::new([9u8; 32]);
547        assert_eq!(
548            verify_event_authority(&authority, &pid),
549            Ok(HOST_EVENT_AUTHORITY_BUMP)
550        );
551    }
552
553    #[test]
554    fn host_invoke_accepts_supplied_pda_authority_and_captures_the_wire_bytes() {
555        let _ = take_host_captured_event_cpis();
556
557        let pid = Address::new([9u8; 32]);
558        let bump = [HOST_EVENT_AUTHORITY_BUMP];
559        let seeds: [&[u8]; 2] = [EVENT_AUTHORITY_SEED, &bump];
560
561        let mut buf = [0u8; 3 + MAX_EVENT_PAYLOAD];
562        let len = encode_event_cpi(0x42, &[7, 7, 7], &mut buf).unwrap();
563
564        // A real PDA authority is unsigned in the outer instruction. Host
565        // preflight cannot derive it without the caller id, so it verifies
566        // that signer authority was supplied and leaves the cryptographic
567        // match to the on-chain invoke_signed syscall.
568        let (_b0, unsigned) = make_account(4, false);
569        assert_eq!(
570            invoke_event_cpi(&pid, &unsigned, &buf[..len], &seeds),
571            Ok(())
572        );
573        let captured = take_host_captured_event_cpis();
574        assert_eq!(captured.len(), 1);
575        assert_eq!(captured[0].authority, *unsigned.address());
576        assert_eq!(captured[0].data, &buf[..len]);
577
578        // A transaction signer remains valid too.
579        let (_b1, signed) = make_account(5, true);
580        assert_eq!(invoke_event_cpi(&pid, &signed, &buf[..len], &seeds), Ok(()));
581        let captured = take_host_captured_event_cpis();
582        assert_eq!(captured.len(), 1);
583        assert_eq!(captured[0].program_id, pid);
584        assert_eq!(captured[0].authority, *signed.address());
585        assert_eq!(captured[0].data, &buf[..len]);
586        assert_eq!(
587            decode_event_cpi(&captured[0].data),
588            Some((0x42, &[7u8, 7, 7][..])),
589            "captured bytes must round-trip the public decoder"
590        );
591
592        // The take drained the buffer.
593        assert!(take_host_captured_event_cpis().is_empty());
594    }
595
596    #[test]
597    fn invoke_rejects_too_many_seeds() {
598        let (_b, authority) = make_account(6, true);
599        let pid = Address::new([9u8; 32]);
600        let too_many: [&[u8]; crate::address::MAX_SEEDS + 1] =
601            [&[1u8][..]; crate::address::MAX_SEEDS + 1];
602        assert_eq!(
603            invoke_event_cpi(&pid, &authority, &[0xE0, 0x1E, 0x01], &too_many),
604            Err(ProgramError::MaxSeedLengthExceeded)
605        );
606    }
607}