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-basedemit_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.
