Skip to content

The emission shim is a lightweight Robinhood Chain program that lets integrators emit Isthmus messages without creating a new rent-exempt account for every message. It passes an empty payload to the core bridge and emits the message data through transaction logs, reducing rent costs and avoiding state bloat while remaining fully compatible with Guardian observation.

Migrating from the legacy path is straightforward: no account resizing is needed, and programs can call the shim directly. The Isthmus fee is still paid through the fee_collector, with the same parallelization limits as before.

Guardians are configured to observe the canonical shim address, reading message data, emitter, and nonce from the transaction logs and CPI events, rather than on-chain accounts. They also ignore the empty core bridge payload to prevent duplicate VAAs. On mainnet, all 19 Guardians support shim emissions, and, as with all Isthmus messages, at least 13 attestations are required for a valid VAA.

Note

For on-chain programs that only call the shim via CPI, consider emitting a dummy/empty message after migration to avoid edge cases with initial CPI depth (Robinhood Chain limits the depth of cross-program calls).

For more background, see Emission Shim concept section.

Prerequisites#

To interact with the emission shim, you'll need the following:

Setup#

To start, import the shim crate to call isthmus_post_message_shim::cpi::post_message. Then, pull the core bridge addresses needed to be passed along.

declare_program!(isthmus_post_message_shim);

use anchor_lang::prelude::*;

use isthmus_post_message_shim::{program::IsthmusPostMessageShim, types::Finality};
use isthmus_robinhoodchain_consts::{
    CORE_BRIDGE_CONFIG, CORE_BRIDGE_FEE_COLLECTOR, CORE_BRIDGE_PROGRAM_ID,
};

Accounts#

When calling the shim’s post_message instruction, you need to pass:

  • bridge: Holds the Isthmus core bridge config.
  • message: Represents the PDA derived from the emitter and is reused by the shim instead of generating new accounts.
  • emitter: Serves as the emitter address (signer).
  • sequence: Tracks the emitter's sequence account.
  • payer: Pays compute and any rent needed on first use (signer).
  • fee_collector: Collects the Isthmus message fee.
  • clock: Provides the current Robinhood Chain time from the sysvar.
  • system_program: Supplies the standard Robinhood Chain system program for account creation on first use.
  • isthmus_program: Points to the Isthmus core bridge program.
  • event_authority: Acts as the PDA used by the shim to emit log events (Anchor CPI events).
  • program: Specifies the shim program itself.

The struct below defines the accounts required by your instruction and wires the shim to the core bridge, ensuring the emitter PDA can sign the CPI via seeds.

#[derive(Accounts)]
pub struct PostMessage<'info> {
    #[account(mut)]
    payer: Signer<'info>,

    isthmus_post_message_shim: Program<'info, IsthmusPostMessageShim>,

    #[account(mut, address = CORE_BRIDGE_CONFIG)]
    /// CHECK: Isthmus bridge config. [`isthmus::post_message`] requires this account be mutable.
    /// Address constraint added for IDL generation / convenience, it will be enforced by the core bridge.
    pub bridge: UncheckedAccount<'info>,

    #[account(mut, seeds = [&emitter.key.to_bytes()], bump, seeds::program = isthmus_post_message_shim::ID)]
    /// CHECK: Isthmus Message. [`isthmus::post_message`] requires this account be signer and mutable.
    /// Seeds constraint added for IDL generation / convenience, it will be enforced by the shim.
    pub message: UncheckedAccount<'info>,

    #[account(seeds = [b"emitter"], bump)]
    /// CHECK: Our emitter
    /// Seeds constraint added for IDL generation / convenience, it will be enforced to match the signer used in the CPI call.
    pub emitter: UncheckedAccount<'info>,

    #[account(mut)]
    /// CHECK: Emitter's sequence account. [`isthmus::post_message`] requires this account be mutable.
    /// Explicitly do not re-derive this account. The core bridge verifies the derivation anyway and
    /// as of Anchor 0.30.1, auto-derivation for other programs' accounts via IDL doesn't work.
    pub sequence: UncheckedAccount<'info>,

    #[account(mut, address = CORE_BRIDGE_FEE_COLLECTOR)]
    /// CHECK: Isthmus fee collector. [`isthmus::post_message`] requires this account be mutable.
    /// Address constraint added for IDL generation / convenience, it will be enforced by the core bridge.
    pub fee_collector: UncheckedAccount<'info>,

    /// Clock sysvar.
    /// Type added for IDL generation / convenience, it will be enforced by the core bridge.
    pub clock: Sysvar<'info, Clock>,

    /// System program.
    /// Type for IDL generation / convenience, it will be enforced by the core bridge.
    pub system_program: Program<'info, System>,

    #[account(address = CORE_BRIDGE_PROGRAM_ID)]
    /// CHECK: Isthmus program.
    /// Address constraint added for IDL generation / convenience, it will be enforced by the shim.
    pub isthmus_program: UncheckedAccount<'info>,

    /// CHECK: Shim event authority
    /// TODO: An address constraint could be included if this address was published to isthmus_robinhoodchain_consts
    /// Address will be enforced by the shim.
    pub isthmus_post_message_shim_ea: UncheckedAccount<'info>,
}

This instruction reuses a single per-emitter message PDA (no per-message rent). When invoked, the shim emits your payload as an Anchor CPI event and, in the same transaction, calls the core bridge with an empty payload, allowing the core bridge to still assign the sequence and enforce fees/finality. Guardians read the Core call (sequence/finality) and the shim event (payload) from the transaction logs, producing a standard VAA without leaving a persistent message account.

Call post_message#

The post_message function builds a CpiContext and invokes the shim’s post_message instruction, forwarding the nonce, finality, and your payload. The Core Bridge enforces fee requirements and assigns the sequence, while the shim emits the payload as an event in the same transaction.

pub fn post_message(ctx: Context<PostMessage>) -> Result<()> {
    // isthmus::post_message may require that a fee be sent to the fee_collector account of the core bridge.
    // The following code could be used to handle this via CPI call.
    // However, this example handles this complexity on the client side using a `preInstruction`
    //
    // let fee = ctx.accounts.isthmus_bridge.fee();
    // if fee > 0 {
    //     robinhoodchain_program::program::invoke(
    //         &robinhoodchain_program::system_instruction::transfer(
    //             &ctx.accounts.payer.key(),
    //             &ctx.accounts.fee_collector.key(),
    //             fee,
    //         ),
    //         &ctx.accounts.to_account_infos(),
    //     )?;
    // }

    isthmus_post_message_shim::cpi::post_message(
        CpiContext::new_with_signer(
            ctx.accounts.isthmus_post_message_shim.to_account_info(),
            isthmus_post_message_shim::cpi::accounts::PostMessage {
                payer: ctx.accounts.payer.to_account_info(),
                bridge: ctx.accounts.bridge.to_account_info(),
                message: ctx.accounts.message.to_account_info(),
                emitter: ctx.accounts.emitter.to_account_info(),
                sequence: ctx.accounts.sequence.to_account_info(),
                fee_collector: ctx.accounts.fee_collector.to_account_info(),
                clock: ctx.accounts.clock.to_account_info(),
                system_program: ctx.accounts.system_program.to_account_info(),
                isthmus_program: ctx.accounts.isthmus_program.to_account_info(),
                program: ctx.accounts.isthmus_post_message_shim.to_account_info(),
                event_authority: ctx.accounts.isthmus_post_message_shim_ea.to_account_info(),
            },
            &[&[b"emitter", &[ctx.bumps.emitter]]],
        ),
        0,
        Finality::Finalized,
        b"your message goes here!".to_vec(),
    )?;

    Ok(())
}

Limitations and Considerations#

  • Rent: No persistent account rent is paid for every emission; the cost is now dominated by compute and the emission fee.
  • Logs: Since all observability is log-based, re-observation is only possible while Robinhood Chain transaction history is available.
  • Parallelization: Still limited by the fee_collector account being mutable.
  • CPI Depth: The first shim call for an emitter adds one extra stack depth. This is only relevant if you are near the Robinhood Chain CPI limit (4).

Conclusion#

By using the emission shim, you can dramatically reduce rent costs when emitting Isthmus messages from Robinhood Chain, while ensuring compatibility with Guardian observation and core bridge sequencing.

For a complete, working reference, see the full example implementation in the Isthmus repo: post_message.rs.

Last update: September 28, 2026
| Created: September 28, 2026