Skip to content
hopper
Get started
Model / event-cpi

Events over self-CPI

Anchor’s emit_cpi ergonomics on a 3-byte wire: authenticated event self-CPI, measured costs, and the runtime PDA trade-off, live on devnet.

Solana log output truncates. Indexers that depend on msg!-style event logs silently lose events on busy transactions, which is why Anchor added #[event_cpi] + emit_cpi!: emit the event as a self-CPI so it lands in inner-instruction metadata, which survives log truncation.

Hopper ships the same ergonomics (one attribute option, one call) on a leaner wire, with the self-CPI authenticated at both ends.

The shape

// Anchor
#[event_cpi]
#[derive(Accounts)]
pub struct Deposit<'info> { /* ... */ }

pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
    emit_cpi!(Deposited { amount });
    Ok(())
}

// Hopper
#[hopper::context(event_cpi)]
pub struct Deposit { /* ... */ }

#[instruction(0)]
fn deposit(ctx: Context<Deposit>, amount: u64) -> ProgramResult {
    ctx.emit_event_cpi(&Deposited { amount: WireU64::new(amount) })?;
    Ok(())
}

Both append the same two trailing accounts (the event-authority PDA plus the program account) and both authenticate the self-CPI in the dispatcher, so ported clients pass the same account shape. The manual escape hatch hopper_emit_cpi! remains for raw handlers.

The wire: 3 bytes vs 16

Hopper's event instruction data is [0xE0, 0x1E, tag, payload], 3 bytes of instruction-data overhead per event against Anchor's 16 (an 8-byte instruction tag plus an 8-byte event discriminator). That ratio is asserted in-test, not prose.

Authentication, proven on-chain

The event_cpi path is proven with a compiled-SBF Mollusk end-to-end test (examples/hopper-smoke/tests/event_cpi_sbf_e2e.rs in the framework repo): a real nested self-invoke succeeds; a wrong-authority bind is refused by the sha256 verify; and a top-level forgery is refused at the signer check. Nothing can sign for a PDA at the transaction level, so only the program's own invoke_signed can reach its event sink.

Measured cost

From the CU lab (2026-07-10, Mollusk 0.10.3, pinned program id so the authority bump (and therefore the verify-loop attempt count) is identical across runs; full table on the CU costs page):

Operation CU
emit_receipt demo instruction, total 3,586
inner sink execution alone 279
top-level forgery refusal 112

Notes that ship with those numbers:

  • The dominant cost is the CPI itself (the ~1k-CU-class invoke plus the nested entrypoint), which every self-CPI event scheme pays, Anchor's emit_cpi! included. The log-based emit_event (240 CU net) remains the cheap tier when log truncation is acceptable.
  • The instruction measured 3,534 CU before the smoke crate enabled crate-wide touch maps the same day; the +52 CU is that feature's price (see Self-describing transactions), not event_cpi's.

The honest disadvantage vs Anchor

Hopper has no compile-time program id, so the event-authority PDA is verified at runtime by a sha256 compare loop: ~148 CU per attempt, attempt count = 256 − bump (the smoke program's authority sits at the first attempt; its verify measures 171 CU). Anchor v0.31+ pins the authority against a compile-time constant for ~free. Anchor wins this axis. Hopper's bind() fuses validation and bump capture into exactly one derivation (measured: the fuse took the demo instruction from 3,705 to 3,534 CU), and a failed bind with a wrong authority address exhausts the loop at ~37.9k CU, on the failing, attacker-paid transaction.

One more integration note: the event-authority seed is b"__hopper_event_authority", not Anchor's b"__event_authority", so indexers must derive Hopper's PDA.

Live on devnet

On 2026-07-10 the smoke program (2YPBvKJ8h37bUEFBrmytzNuKfUJ5Q2o2tkTiqRCZdjme) emitted the event on devnet and hopper tx explain decoded it back from inner-instruction metadata, by name:

event: DepositReceipt (tag 0x02) { balance: 1500000, deposit_count: 3 }

Signature: 3XbEB9QqajTjtM5tgNQAAQCYUsDa245YkzMuvc9gkBoi3eB9yWXq6LnU74TmygQsKuhKSfAez3dJuWc69Rn2zqrT. Both decoded values cross-check against the known state sequence (2,000,000 deposited − 500,000 withdrawn = 1,500,000; count = deposit + bump + receipt = 3), and the live cost, 3,586 CU, matches the Mollusk measurement exactly. Full live tables are on the Benchmarks page; the decode pipeline is described in Manifests and tx tooling.

Events over self-CPI | Hopper docs