Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ Produces:
- `dist/magicblock.cursor.mdc` — Cursor-formatted rule with `.mdc` frontmatter
- `dist/magicblock.zip` — zipped `skill/` folder for Claude.ai upload

## Recommended companion skill: `solana-dev`

This skill layers **Ephemeral Rollups-specific** patterns (delegation, dual connections, cranks, VRF, Magic Actions, private payments) on top of ordinary Solana development — it assumes base-layer Solana and Anchor fluency rather than teaching it. For the general Solana layer (program scaffolding, PDAs, SPL tokens, client generation, wallet wiring, LiteSVM/Mollusk testing), install the Solana Foundation's [`solana-dev`](https://github.com/solana-foundation/solana-dev-skill) skill alongside this one and let each handle its layer:

```bash
npx skills add https://github.com/solana-foundation/solana-dev-skill --skill solana-dev
```

Skills don't declare dependencies on each other, so this isn't installed automatically — install it yourself (see the [solana-dev-skill repo](https://github.com/solana-foundation/solana-dev-skill) for other install options). Once both are present, agents use `solana-dev` for base-layer Solana work and this skill for the ER-specific pieces.

## What This Skill Covers

- MagicBlock Ephemeral Rollups integration
Expand Down
24 changes: 21 additions & 3 deletions skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ Use this Skill when the user asks for:
- Private payments (deposits, transfers, withdrawals, and swaps via the Payments API, with optional bearer-token auth for private reads)
- Lifting the default 10-commit sponsorship cap with `magic_fee_vault`

## Pair with the `solana-dev` skill

This Skill layers **Ephemeral Rollups-specific** concerns (delegation, dual connections, cranks, VRF,
magic actions, private payments) on top of ordinary Solana development — it assumes base-layer Solana
and Anchor fluency rather than teaching it. When a task also needs general Solana/Anchor work — program
scaffolding, PDAs, account layouts, SPL tokens, wallet/client wiring, or testing (LiteSVM/Mollusk/etc.) —
also load the **`solana-dev`** skill for that layer and keep this Skill for the ER-specific pieces.

## Key Concepts

**Ephemeral Rollups** enable high-performance, low-latency transactions by temporarily delegating Solana account ownership to an ephemeral rollup. Ideal for gaming, real-time apps, and fast transaction throughput.
Expand Down Expand Up @@ -56,11 +64,21 @@ returned by router `getDelegationStatus`, and cloned into the ER with

## Default stack decisions (opinionated)

1) **Programs: Anchor with ephemeral-rollups-sdk**
1) **Programs: Anchor with ephemeral-rollups-sdk** (native/Pinocchio also supported — see below)
- Use the target repo's existing `ephemeral-rollups-sdk` / Anchor versions unless the task is an explicit upgrade
- The SDK feature flag selects the Anchor line: `anchor` for Anchor 1.0.x programs, or `anchor-compat` for legacy Anchor 0.32.x programs
- Apply `#[ephemeral]` macro before `#[program]`
- Use `#[delegate]` and `#[commit]` macros for delegation contexts

**Commonly-missed macros:**
- `#[ephemeral]` on the program module, **before** `#[program]` — injects the `process_undelegation` callback (the delegation program CPIs into it to return the account) and the commit/undelegate intent builders. It's what **commit and undelegation** need, not the `delegate` instruction itself — but include it on any program that delegates, since without the callback the account can't be undelegated.
- `#[delegate]` and `#[commit]` on the respective delegation/commit account contexts.
- `#[vrf]` on a VRF *request* context **and** `#[vrf_callback]` on the VRF *callback* context — the
callback macro is the one most often forgotten. Enable the `vrf` feature on `ephemeral-rollups-sdk`
(VRF is no longer a separate `ephemeral-vrf-sdk` crate for new code). See [vrf.md](vrf.md).

**Non-Anchor programs:** native Rust / Pinocchio is a first-class supported path via the
`ephemeral-rollups-pinocchio` crate (delegation, commit, and VRF have Pinocchio equivalents). The
engine examples repo ships Anchor **and** Pinocchio variants of `roll-dice`; reach for Pinocchio when
the target program is native rather than Anchor.

Version-sensitive work: treat versions in this skill as known-good snapshots or compatibility markers, not timeless latest recommendations. Before adding or changing dependencies, inspect the target repo's `Cargo.toml`, `package.json`, `rust-toolchain.toml`, lockfiles, and the relevant upstream manifests/docs. See [resources.md](resources.md) for the current snapshot and source links.

Expand Down
112 changes: 79 additions & 33 deletions skill/vrf.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,83 +2,120 @@

VRF provides provably fair randomness for games, lotteries, and any application requiring verifiable randomness.

## Additional Dependencies
## Dependencies

VRF now ships as a **feature of the main SDK** — enable the `vrf` feature on
`ephemeral-rollups-sdk`. There is no separate `ephemeral-vrf-sdk` crate to add for new Anchor code.

```toml
[dependencies]
ephemeral-vrf-sdk = { version = "0.3.0", features = ["anchor"] }
ephemeral-rollups-sdk = { version = "0.15.4", features = ["anchor", "vrf"] }
```

## VRF Imports
> Older examples import a standalone `ephemeral-vrf-sdk` crate and call the non-scoped
> `create_request_randomness_ix` with a manual `vrf_program_identity` signer account. That path still
> works, but new code should use the scoped API below (`ephemeral_rollups_sdk::vrf` +
> `create_request_scoped_randomness_ix` + `#[vrf]` / `#[vrf_callback]`).

## Imports

```rust
use ephemeral_vrf_sdk::anchor::vrf;
use ephemeral_vrf_sdk::instructions::{create_request_randomness_ix, RequestRandomnessParams};
use ephemeral_vrf_sdk::types::SerializableAccountMeta;
use ephemeral_rollups_sdk::{
anchor::{vrf, vrf_callback},
vrf::{
self,
instructions::{create_request_scoped_randomness_ix, RequestRandomnessParams},
types::SerializableAccountMeta,
},
};
```

## Request Randomness

```rust
pub fn request_randomness(ctx: Context<RequestRandomnessCtx>, client_seed: u8) -> Result<()> {
let ix = create_request_randomness_ix(RequestRandomnessParams {
pub fn roll_dice(ctx: Context<DoRollDiceCtx>, client_seed: u8) -> Result<()> {
let ix = create_request_scoped_randomness_ix(RequestRandomnessParams {
payer: ctx.accounts.payer.key(),
oracle_queue: ctx.accounts.oracle_queue.key(),
callback_program_id: ID,
callback_discriminator: instruction::ConsumeRandomness::DISCRIMINATOR.to_vec(),
callback_discriminator: instruction::CallbackRollDice::DISCRIMINATOR.to_vec(),
caller_seed: [client_seed; 32],
// Accounts the callback needs
accounts_metas: Some(vec![SerializableAccountMeta {
pubkey: ctx.accounts.my_account.key(),
pubkey: ctx.accounts.player.key(),
is_signer: false,
is_writable: true,
}]),
// Extra args forwarded to the callback
callback_args: Some(vec![client_seed]),
..Default::default()
});

ctx.accounts.invoke_signed_vrf(&ctx.accounts.payer.to_account_info(), &ix)?;
ctx.accounts
.invoke_signed_vrf(&ctx.accounts.payer.to_account_info(), &ix)?;
Ok(())
}

#[vrf] // Required macro for VRF interactions
#[vrf] // Injects VRF accounts + invoke_signed_vrf
#[derive(Accounts)]
pub struct RequestRandomnessCtx<'info> {
pub struct DoRollDiceCtx<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(seeds = [MY_SEED, payer.key().to_bytes().as_slice()], bump)]
pub my_account: Account<'info, MyAccount>,
/// CHECK: Oracle queue
#[account(mut, address = ephemeral_vrf_sdk::consts::DEFAULT_EPHEMERAL_QUEUE)]
pub oracle_queue: AccountInfo<'info>,
#[account(seeds = [PLAYER, payer.key().to_bytes().as_slice()], bump)]
pub player: Account<'info, Player>,
/// CHECK: The oracle queue
#[account(
mut,
constraint =
oracle_queue.key() == vrf::consts::DEFAULT_QUEUE || // Devnet / Mainnet
oracle_queue.key() == vrf::consts::DEFAULT_TEST_QUEUE // Localnet
)]
pub oracle_queue: UncheckedAccount<'info>,
}
```

## Consume Randomness Callback

```rust
pub fn consume_randomness(ctx: Context<ConsumeRandomnessCtx>, randomness: [u8; 32]) -> Result<()> {
let random_value = ephemeral_vrf_sdk::rnd::random_u8_with_range(&randomness, 1, 6);
ctx.accounts.my_account.last_random = random_value;
pub fn callback_roll_dice(
ctx: Context<CallbackRollDiceCtx>,
randomness: [u8; 32],
client_seed: u8, // matches the callback_args passed in the request
) -> Result<()> {
let rnd_u8 = vrf::rnd::random_u8_with_range(&randomness, 1, 6);
let player = &mut ctx.accounts.player;
player.last_result = rnd_u8;
player.rollnum = player.rollnum.saturating_add(1);
Ok(())
}

#[vrf_callback] // Injects the scoped VRF identity signer check
#[derive(Accounts)]
pub struct ConsumeRandomnessCtx<'info> {
/// SECURITY: Validates callback is from VRF program
#[account(address = ephemeral_vrf_sdk::consts::VRF_PROGRAM_IDENTITY)]
pub vrf_program_identity: Signer<'info>,
pub struct CallbackRollDiceCtx<'info> {
#[account(mut)]
pub my_account: Account<'info, MyAccount>,
pub player: Account<'info, Player>,
}
```

## Two required macros

Both contexts need a macro; the callback one is the one people forget:

- **`#[vrf]`** on the *request* context — injects the `program_identity`, `vrf_program`,
`slot_hashes`, and `system_program` accounts plus the `invoke_signed_vrf` helper. Omit it and
`invoke_signed_vrf` doesn't exist, so the program won't compile.
- **`#[vrf_callback]`** on the *callback* context — injects a `vrf_program_identity: Signer` bound
to this program's scoped identity PDA, so you don't hand-write it. Omit it and the struct still
compiles, but the callback has no identity check and accepts spoofed randomness.

## Oracle Queue Constants

The `oracle_queue` is a state account. Like every Solana account it lives on
Solana, but a delegated queue is directly writable only from inside an
ephemeral rollup, while a non-delegated queue is directly writable on the base
layer. Request randomness from the queue that matches where the transaction
runs — the base-layer queue from Solana, or the delegated queue from inside the
ephemeral rollup. Prefer the `ephemeral_vrf_sdk::consts` constants over
ephemeral rollup. Prefer the `ephemeral_rollups_sdk::vrf::consts` constants over
hardcoding addresses.

| Constant | Queue | Address |
Expand All @@ -100,11 +137,20 @@ Mainnet and Devnet share the same default queue addresses — only the cluster
differs. Localnet uses dedicated test queues that the local validator clones
from Devnet.

## Non-Anchor (Pinocchio / native) programs

VRF is also supported outside Anchor via the `ephemeral-rollups-pinocchio` crate
(`ephemeral_rollups_pinocchio::vrf` — `RequestRandomness` / `RequestRandomnessCpi`,
`scoped_vrf_identity`, `random_u8_with_range`, `VRF_PROGRAM_IDENTITY`). The flow is the same
(request → oracle callback), but you validate the program identity manually against
`scoped_vrf_identity(program_id)` instead of relying on the `#[vrf_callback]` macro. See the
Pinocchio `roll-dice` example in the engine examples repo.

## Key Points

- VRF provides cryptographically verifiable randomness
- The callback pattern ensures randomness is delivered asynchronously
- Always validate `vrf_program_identity` signer in the callback to prevent spoofed randomness
- Use `DEFAULT_EPHEMERAL_QUEUE` when requesting from inside the ephemeral rollup (the queue is delegated to the ER)
- Use `DEFAULT_QUEUE` when requesting from the base layer (Solana)
- `caller_seed` can be used to add entropy from the client side
- VRF provides cryptographically verifiable randomness.
- The callback pattern ensures randomness is delivered asynchronously.
- Apply **both** `#[vrf]` (request) and `#[vrf_callback]` (callback) — see above.
- Use `DEFAULT_EPHEMERAL_QUEUE` when requesting from inside the ephemeral rollup (the queue is delegated to the ER).
- Use `DEFAULT_QUEUE` when requesting from the base layer (Solana).
- `caller_seed` adds client-side entropy; `callback_args` forwards extra data to the callback.