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
37 changes: 37 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ members = [
"basics/create-account/anchor/programs/create-system-account",
"basics/create-account/asm",
"basics/cross-program-invocation/anchor/programs/*",
"basics/cross-program-invocation/pinocchio/programs/hand",
"basics/cross-program-invocation/pinocchio/programs/lever",
"basics/hello-solana/native/program",
"basics/hello-solana/anchor/programs/*",
"basics/hello-solana/pinocchio/program",
Expand All @@ -41,6 +43,7 @@ members = [
"basics/favorites/native/program",
"basics/favorites/pinocchio/program",
"basics/repository-layout/native/program",
"basics/repository-layout/pinocchio/program",
"basics/repository-layout/anchor/programs/*",
"basics/transfer-sol/native/program",
"basics/transfer-sol/pinocchio/program",
Expand Down
23 changes: 23 additions & 0 deletions basics/cross-program-invocation/pinocchio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Cross Program Invocation: Solana Pinocchio

A [Cross Program Invocation (CPI)](https://solana.com/docs/core/cpi) example written using the [Pinocchio](https://github.com/anza-xyz/pinocchio) framework with only the Solana toolchain.

Two programs work together:

- `lever` - owns a `power` account that stores a single on/off byte. It exposes `initialize` (create the account) and `switch_power` (flip the byte and log who pulled it).
- `hand` - takes a name, then invokes `lever`'s `switch_power` instruction via CPI, forwarding the name.

Because Pinocchio runs in `no_std` without an allocator, `hand` builds the CPI instruction buffer on the stack and caps the forwarded name length.

## Setup

1. Build both [programs](https://solana.com/docs/terminology#program):
- `cargo build-sbf --manifest-path=./programs/hand/Cargo.toml`
- `cargo build-sbf --manifest-path=./programs/lever/Cargo.toml`
2. Run the Rust + LiteSVM tests: `cargo test --manifest-path=./programs/lever/Cargo.toml`

The tests exercise the full `initialize -> pull -> pull-again` CPI flow plus an invalid-discriminator rejection case. Rebuild the programs after every change before re-running the tests: the tests embed each `.so` at compile time, so a stale binary silently tests old code.

## Credits

Ported from the [Pinocchio cross-program-invocation example](https://github.com/solana-developers/program-examples/pull/584) contributed by [@MarkFeder](https://github.com/MarkFeder) to solana-developers/program-examples.
18 changes: 18 additions & 0 deletions basics/cross-program-invocation/pinocchio/programs/hand/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "cross-program-invocation-pinocchio-hand"
version = "0.1.0"
edition = "2021"

[dependencies]
pinocchio.workspace = true

[lib]
name = "cross_program_invocation_pinocchio_hand"
crate-type = ["cdylib", "lib"]

[features]
custom-heap = []
custom-panic = []

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }
48 changes: 48 additions & 0 deletions basics/cross-program-invocation/pinocchio/programs/hand/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#![no_std]

use pinocchio::{
cpi::invoke,
entrypoint,
error::ProgramError,
instruction::{InstructionAccount, InstructionView},
nostd_panic_handler, AccountView, Address, ProgramResult,
};

entrypoint!(process_instruction);
nostd_panic_handler!();

// Matches lever's switch_power discriminator.
const LEVER_IX_SWITCH_POWER: u8 = 1;

// Cap the forwarded name length so we can build the CPI buffer on the stack
// (pinocchio runs in `no_std` without an allocator by default).
const MAX_NAME_LEN: usize = 128;
const CPI_DATA_BUF: usize = MAX_NAME_LEN + 1;

fn process_instruction(
_program_id: &Address,
accounts: &[AccountView],
instruction_data: &[u8],
) -> ProgramResult {
let [power, lever_program] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};

let name = instruction_data;
if name.len() > MAX_NAME_LEN {
return Err(ProgramError::InvalidInstructionData);
}

let mut cpi_data = [0u8; CPI_DATA_BUF];
cpi_data[0] = LEVER_IX_SWITCH_POWER;
cpi_data[1..1 + name.len()].copy_from_slice(name);

let metas = [InstructionAccount::writable(power.address())];
let ix = InstructionView {
program_id: lever_program.address(),
accounts: &metas,
data: &cpi_data[..1 + name.len()],
};

invoke::<1>(&ix, &[power])
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "cross-program-invocation-pinocchio-lever"
version = "0.1.0"
edition = "2021"

[dependencies]
pinocchio.workspace = true
pinocchio-log.workspace = true
pinocchio-system.workspace = true

[lib]
name = "cross_program_invocation_pinocchio_lever"
crate-type = ["cdylib", "lib"]

[features]
custom-heap = []
custom-panic = []

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }

[dev-dependencies]
litesvm = "0.11.0"
solana-instruction = "3.0.0"
solana-keypair = "3.0.1"
solana-native-token = "3.0.0"
solana-pubkey = "3.0.0"
solana-transaction = "3.0.1"
solana-system-interface.workspace = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#![no_std]

use pinocchio::{
entrypoint,
error::ProgramError,
nostd_panic_handler,
sysvars::{rent::Rent, Sysvar},
AccountView, Address, ProgramResult,
};
use pinocchio_log::log;
use pinocchio_system::instructions::CreateAccount;

entrypoint!(process_instruction);
nostd_panic_handler!();

// Single-byte account: stores `is_on` as 0 or 1.
const POWER_ACCOUNT_SPACE: u64 = 1;

// Instruction discriminators
const IX_INITIALIZE: u8 = 0;
const IX_SWITCH_POWER: u8 = 1;

fn process_instruction(
program_id: &Address,
accounts: &[AccountView],
instruction_data: &[u8],
) -> ProgramResult {
match instruction_data.split_first() {
Some((&IX_INITIALIZE, _)) => initialize(program_id, accounts),
Some((&IX_SWITCH_POWER, name)) => switch_power(accounts, name),
_ => Err(ProgramError::InvalidInstructionData),
}
}

fn initialize(program_id: &Address, accounts: &[AccountView]) -> ProgramResult {
let [power, user, _system_program] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};

let lamports = Rent::get()?.try_minimum_balance(POWER_ACCOUNT_SPACE as usize)?;

CreateAccount {
from: user,
to: power,
lamports,
space: POWER_ACCOUNT_SPACE,
owner: program_id,
}
.invoke()?;

let mut data = power.try_borrow_mut()?;
data[0] = 0;
Ok(())
}

fn switch_power(accounts: &[AccountView], name: &[u8]) -> ProgramResult {
let [power] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};

let mut data = power.try_borrow_mut()?;
data[0] = if data[0] == 0 { 1 } else { 0 };
let is_on = data[0] == 1;
drop(data);

let name_str = core::str::from_utf8(name).map_err(|_| ProgramError::InvalidInstructionData)?;
log!("{} is pulling the power switch!", name_str);

if is_on {
log!("The power is now on.");
} else {
log!("The power is now off!");
}

Ok(())
}
Loading
Loading