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}