Documentation Index Fetch the complete documentation index at: https://mintlify.com/magicblock-labs/magicblock-engine-examples/llms.txt
Use this file to discover all available pages before exploring further.
A lightweight counter program using the Pinocchio framework with Ephemeral Rollups. This example demonstrates a more efficient alternative to Borsh serialization, using manual serialization with fixed-size types for reduced compute and size overhead.
What You’ll Learn
How to use Pinocchio for lightweight Solana programs
How to implement manual serialization without Borsh
How to delegate accounts using the Pinocchio SDK
How to work with fixed-size arrays and primitives
How to minimize program size and compute units
Program Structure
The Pinocchio counter program includes the following instructions:
0: InitializeCounter - Initialize a counter PDA to 0 (payload: bump u8)
1: IncreaseCounter - Increase counter by specified amount (payload: bump u8 + increase_by u64)
2: Delegate - Delegate the counter to Ephemeral Rollups (payload: bump u8)
3: CommitAndUndelegate - Commit and undelegate the counter
4: Commit - Commit changes to base layer
5: IncrementAndCommit - Increment and commit in one instruction (payload: bump u8 + increase_by u64)
6: IncrementAndUndelegate - Increment and undelegate in one instruction (payload: bump u8 + increase_by u64)
Software Requirements
Software Version Installation Guide Solana 2.3.13 Install Solana Rust 1.85.0 Install Rust Node 24.10.0 Install Node
Build and Test
Run tests
Run tests with logging enabled: cargo test-sbf --features logging
Key Differences from Rust Counter
No Borsh Uses manual serialization with to_le_bytes() and from_le_bytes() for simplicity
No Vec All types use fixed-size arrays or primitives
Pinocchio Framework Leverages Pinocchio’s lightweight instruction handling
Direct State Management Simple Counter struct with manual memory management
Program Implementation
State Definition
The counter state is a simple struct with manual serialization:
use pinocchio :: error :: ProgramError ;
#[repr( C )]
pub struct Counter {
pub count : u64 ,
}
impl Counter {
pub const SIZE : usize = 8 ;
pub fn load_mut ( data : & mut [ u8 ]) -> Result < & mut Self , ProgramError > {
if data . len () < Self :: SIZE {
return Err ( ProgramError :: InvalidArgument );
}
let ptr = data . as_mut_ptr () as * mut Self ;
// Verify alignment
if ( ptr as usize ) % core :: mem :: align_of :: < Self >() != 0 {
return Err ( ProgramError :: InvalidAccountData );
}
// Safety: caller ensures the account data is valid for Counter.
Ok ( unsafe { & mut * ptr })
}
}
Using #[repr(C)] ensures the struct has a predictable memory layout, allowing direct pointer casting.
Initialize Counter
use pinocchio :: { AccountView , Address , ProgramResult };
use pinocchio_system :: instructions :: CreateAccount ;
use pinocchio :: cpi :: { Seed , Signer };
pub fn process_initialize_counter (
program_id : & Address ,
accounts : & [ AccountView ],
bump : u8 ,
) -> ProgramResult {
let [ initializer_account , counter_account , _system_program ] = accounts else {
return Err ( ProgramError :: NotEnoughAccountKeys );
};
let bump_seed = [ bump ];
let counter_pda = counter_address_from_bump ( program_id , initializer_account , bump ) ? ;
if counter_pda != * counter_account . address () {
return Err ( ProgramError :: InvalidArgument );
}
// Create counter account if it doesn't exist.
if counter_account . lamports () == 0 {
let rent_exempt_lamports = 1_000_000 ;
let create_account_ix = CreateAccount {
from : initializer_account ,
to : counter_account ,
lamports : rent_exempt_lamports ,
space : Counter :: SIZE as u64 ,
owner : program_id ,
};
let seed_array : [ Seed ; 3 ] = [
Seed :: from ( b"counter" ),
Seed :: from ( initializer_account . address () . as_ref ()),
Seed :: from ( & bump_seed ),
];
let signer = Signer :: from ( & seed_array );
create_account_ix . invoke_signed ( & [ signer ]) ? ;
}
// Initialize counter to 0.
let mut data = counter_account . try_borrow_mut () ? ;
let counter_data = Counter :: load_mut ( & mut data ) ? ;
counter_data . count = 0 ;
Ok (())
}
Increment Counter
pub fn process_increase_counter (
program_id : & Address ,
accounts : & [ AccountView ],
bump : u8 ,
increase_by : u64 ,
) -> ProgramResult {
let [ initializer_account , counter_account ] = accounts else {
return Err ( ProgramError :: NotEnoughAccountKeys );
};
let counter_pda = counter_address_from_bump ( program_id , initializer_account , bump ) ? ;
if counter_pda != * counter_account . address () {
return Err ( ProgramError :: InvalidArgument );
}
let mut data = counter_account . try_borrow_mut () ? ;
let counter_data = Counter :: load_mut ( & mut data ) ? ;
counter_data . count = counter_data
. count
. checked_add ( increase_by )
. ok_or ( ProgramError :: ArithmeticOverflow ) ? ;
Ok (())
}
Pinocchio uses AccountView instead of AccountInfo, providing a more lightweight abstraction.
Delegation Implementation
use ephemeral_rollups_pinocchio :: instruction :: delegate_account;
use ephemeral_rollups_pinocchio :: types :: DelegateConfig ;
pub fn process_delegate (
_program_id : & Address ,
accounts : & [ AccountView ],
bump : u8 ,
) -> ProgramResult {
let [ initializer , pda_to_delegate , owner_program , delegation_buffer , delegation_record , delegation_metadata , _delegation_program , system_program , rest @ .. ] =
accounts
else {
return Err ( ProgramError :: NotEnoughAccountKeys );
};
let validator = rest . first () . map ( | account | * account . address ());
let seed_1 = b"counter" ;
let seed_2 = initializer . address () . as_ref ();
let seeds : & [ & [ u8 ]] = & [ seed_1 , seed_2 ];
let counter_pda = counter_address_from_bump ( owner_program . address (), initializer , bump ) ? ;
let delegate_config = DelegateConfig {
validator ,
.. Default :: default ()
};
if counter_pda != * pda_to_delegate . address () {
return Err ( ProgramError :: InvalidArgument );
}
delegate_account (
& [
initializer ,
pda_to_delegate ,
owner_program ,
delegation_buffer ,
delegation_record ,
delegation_metadata ,
system_program ,
],
seeds ,
bump ,
delegate_config ,
) ? ;
Ok (())
}
Commit and Undelegate
use ephemeral_rollups_pinocchio :: instruction :: {
commit_accounts, commit_and_undelegate_accounts,
};
pub fn process_commit ( _program_id : & Address , accounts : & [ AccountView ]) -> ProgramResult {
let [ initializer , counter_account , magic_program , magic_context ] = accounts else {
return Err ( ProgramError :: NotEnoughAccountKeys );
};
if ! initializer . is_signer () {
return Err ( ProgramError :: MissingRequiredSignature );
}
commit_accounts (
initializer ,
& [ * counter_account ],
magic_context ,
magic_program ,
) ? ;
Ok (())
}
pub fn process_commit_and_undelegate (
_program_id : & Address ,
accounts : & [ AccountView ],
) -> ProgramResult {
let [ initializer , counter_account , magic_program , magic_context ] = accounts else {
return Err ( ProgramError :: NotEnoughAccountKeys );
};
if ! initializer . is_signer () {
return Err ( ProgramError :: MissingRequiredSignature );
}
commit_and_undelegate_accounts (
initializer ,
& [ * counter_account ],
magic_context ,
magic_program ,
) ? ;
Ok (())
}
TypeScript Client Usage
Initialize with Bump
Pinocchio counter requires passing the bump seed in the instruction data.
import {
Connection ,
getProgramDerivedAddress ,
getAddressEncoder ,
} from '@solana/kit' ;
import * as borsh from "borsh" ;
const addressEncoder = getAddressEncoder ();
const [ counterPda , bump ] = await getProgramDerivedAddress ({
programAddress: PROGRAM_ID ,
seeds: [
Buffer . from ( "counter" ),
addressEncoder . encode ( userPubkey )
],
});
const bumpBytes = Buffer . from ([ bump ]);
const accounts = [
{ address: userPubkey , role: AccountRole . WRITABLE_SIGNER },
{ address: counterPda , role: AccountRole . WRITABLE },
{ address: SYSTEM_PROGRAM_ADDRESS , role: AccountRole . READONLY },
];
const serializedInstructionData = Buffer . concat ([
Buffer . from ( CounterInstruction . InitializeCounter , "hex" ),
bumpBytes ,
]);
const initializeIx : Instruction = {
accounts ,
programAddress: PROGRAM_ID ,
data: serializedInstructionData ,
};
Delegate to Ephemeral Rollups
import {
DELEGATION_PROGRAM_ID ,
delegationRecordPdaFromDelegatedAccount ,
delegationMetadataPdaFromDelegatedAccount ,
delegateBufferPdaFromDelegatedAccountAndOwnerProgram ,
} from "@magicblock-labs/ephemeral-rollups-kit" ;
const remainingAccounts = connection . clusterUrlHttp . includes ( "localhost" )
? [{
address: address ( "mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev" ),
role: AccountRole . READONLY
}]
: [{
address: address ( "MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57" ),
role: AccountRole . READONLY
}];
const accounts = [
{ address: userPubkey , role: AccountRole . WRITABLE_SIGNER },
{ address: counterPda , role: AccountRole . WRITABLE },
{ address: PROGRAM_ID , role: AccountRole . READONLY },
{
address: await delegateBufferPdaFromDelegatedAccountAndOwnerProgram ( counterPda , PROGRAM_ID ),
role: AccountRole . WRITABLE
},
{
address: await delegationRecordPdaFromDelegatedAccount ( counterPda ),
role: AccountRole . WRITABLE
},
{
address: await delegationMetadataPdaFromDelegatedAccount ( counterPda ),
role: AccountRole . WRITABLE
},
{ address: DELEGATION_PROGRAM_ID , role: AccountRole . READONLY },
{ address: SYSTEM_PROGRAM_ADDRESS , role: AccountRole . READONLY },
... remainingAccounts ,
];
const serializedInstructionData = Buffer . concat ([
Buffer . from ( CounterInstruction . Delegate , "hex" ),
bumpBytes ,
]);
Execute on Ephemeral Rollups
Increment
Commit
Undelegate
const accounts = [
{ address: userPubkey , role: AccountRole . WRITABLE_SIGNER },
{ address: counterPda , role: AccountRole . WRITABLE },
];
const serializedInstructionData = Buffer . concat ([
Buffer . from ( CounterInstruction . IncreaseCounter , "hex" ),
bumpBytes ,
borsh . serialize (
IncreaseCounterPayload . schema ,
new IncreaseCounterPayload ( 1 )
),
]);
const increaseCounterIx : Instruction = {
accounts ,
programAddress: PROGRAM_ID ,
data: serializedInstructionData ,
};
const transactionMessage = pipe (
createTransactionMessage ({ version: 0 }),
tx => setTransactionMessageFeePayer ( userPubkey , tx ),
tx => appendTransactionMessageInstructions ([ increaseCounterIx ], tx )
);
const txHash = await ephemeralConnection . sendAndConfirmTransaction (
transactionMessage ,
[ userKeypair ],
{ commitment: "confirmed" , skipPreflight: true }
);
Smaller Program Size No Borsh dependency reduces compiled program size
Lower Compute Units Direct memory access is more efficient than serialization
Minimal Dependencies Pinocchio is lightweight with fewer external crates
Fixed Allocations No Vec types means predictable memory usage
Pinocchio requires manual memory management and is more error-prone than frameworks like Anchor. Use it when you need maximum performance and minimal size.
Account Structure
The Counter account is simple:
Size : 8 bytes
Layout : Single u64 count value
Serialization : Direct memory casting (no Borsh)
Source Code
View the complete source code on GitHub:
pinocchio-counter on GitHub