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.

This reference covers Hopper 0.5.0, published on crates.io. See the release status for package verification, dated benchmarks, and remaining audit work.

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: Ctx<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 in compiled-SBF tests

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 dated derivation comparison

This dated smoke fixture derives its event-authority PDA at runtime. It 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). The reviewed Anchor v0.31 implementation pins the authority against a compile-time constant for ~free. Anchor wins this axis in that comparison. 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.