Skip to content

Commit

Permalink
Add Rust programs using pinocchio (#7)
Browse files Browse the repository at this point in the history
* Add pinocchio programs

* Bypass borrow check

* Use lazy entrypoint

* Update pinocchio results

* Update crate version

* Remove duplicated tests

* Add pinocchio test script

* Update CU values

* Clean up

* Add number of accounts assert

* Simplify program name env
  • Loading branch information
febo authored Oct 31, 2024
1 parent 8fe1bf0 commit 45d9595
Show file tree
Hide file tree
Showing 12 changed files with 256 additions and 6 deletions.
33 changes: 33 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,36 @@ jobs:

- name: Build and test program
run: ./test-asm.sh ${{ matrix.program }}

pinocchio-test:
name: Run tests against Pinocchio Rust implementations
strategy:
matrix:
program: [transfer-lamports, cpi]
fail-fast: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
~/.cache/solana
key: rust-${{ hashFiles('./Cargo.lock') }}

- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.78.0

- name: Install Rust build deps
run: ./install-rust-build-deps.sh

- name: Install Solana
run: |
./install-solana.sh
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
- name: Build and test program
run: ./test-pinocchio.sh ${{ matrix.program }}
56 changes: 56 additions & 0 deletions Cargo.lock

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

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
[workspace]
members = [
"cpi",
"cpi/pinocchio",
"helloworld",
"transfer-lamports"
"transfer-lamports",
"transfer-lamports/pinocchio"
]
resolver = "2"

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ lets the VM assume it worked.
| Zig | 43 |
| C | 103 |
| Assembly | 22 |
| Rust (pinocchio) | 23 |

This one starts to get interesting since it requires parsing the instruction
input. Since the assembly version knows exactly where to find everything, it can
Expand All @@ -182,3 +183,4 @@ the address and `invoke_signed` to CPI to the system program.
| Rust | 3662 |
| Zig | 2825 |
| C | 3122 |
| Rust (pinocchio) | 2816 |
11 changes: 11 additions & 0 deletions cpi/pinocchio/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "pinocchio-rosetta-cpi"
version = "1.0.0"
edition = "2021"

[dependencies]
pinocchio = "0.6"
pinocchio-system = "0.2"

[lib]
crate-type = ["cdylib", "lib"]
65 changes: 65 additions & 0 deletions cpi/pinocchio/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! Rust example using pinocchio demonstrating invoking another program
#![deny(missing_docs)]

use pinocchio::{
instruction::{Account, AccountMeta, Instruction},
lazy_entrypoint::InstructionContext,
program::invoke_signed_unchecked,
program_error::ProgramError,
pubkey::create_program_address,
signer, ProgramResult,
};

// Since this is a single instruction program, we use the "lazy" variation
// of the entrypoint.
pinocchio::lazy_entrypoint!(process_instruction);

/// Amount of bytes of account data to allocate
pub const SIZE: usize = 42;

/// Instruction processor.
fn process_instruction(mut context: InstructionContext) -> ProgramResult {
if context.remaining() != 2 {
return Err(ProgramError::NotEnoughAccountKeys);
}

// Account info to allocate and for the program being invoked. We know that
// we got 2 accounts, so it is ok use `next_account_unchecked` twice.
let allocated_info = unsafe { context.next_account_unchecked().assume_account() };
// just move the offset, we don't need the system program info
let _system_program_info = unsafe { context.next_account_unchecked() };

// Again, don't need to check that all accounts have been consumed, we know
// we have exactly 2 accounts.
let (instruction_data, program_id) = unsafe { context.instruction_data_unchecked() };

let expected_allocated_key =
create_program_address(&[b"You pass butter", &[instruction_data[0]]], program_id)?;
if *allocated_info.key() != expected_allocated_key {
// allocated key does not match the derived address
return Err(ProgramError::InvalidArgument);
}

// Invoke the system program to allocate account data
let mut data = [0; 12];
data[0] = 8; // ix discriminator
data[4..12].copy_from_slice(&SIZE.to_le_bytes());

let instruction = Instruction {
program_id: &pinocchio_system::ID,
accounts: &[AccountMeta::writable_signer(allocated_info.key())],
data: &data,
};

// Invoke the system program with the 'unchecked' function - this is ok since
// we know the accounts are not borrowed elsewhere.
unsafe {
invoke_signed_unchecked(
&instruction,
&[Account::from(&allocated_info)],
&[signer!(b"You pass butter", &[instruction_data[0]])],
)
};

Ok(())
}
8 changes: 5 additions & 3 deletions cpi/tests/functional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use {
rent::Rent,
system_program,
},
solana_program_rosetta_cpi::{process_instruction, SIZE},
solana_program_rosetta_cpi::SIZE,
solana_program_test::*,
solana_sdk::{account::Account, signature::Signer, transaction::Transaction},
std::str::FromStr,
Expand All @@ -16,11 +16,13 @@ async fn test_cross_program_invocation() {
let program_id = Pubkey::from_str("invoker111111111111111111111111111111111111").unwrap();
let (allocated_pubkey, bump_seed) =
Pubkey::find_program_address(&[b"You pass butter"], &program_id);

let mut program_test = ProgramTest::new(
"solana_program_rosetta_cpi",
option_env!("PROGRAM_NAME").unwrap_or("solana_program_rosetta_cpi"),
program_id,
processor!(process_instruction),
None,
);

program_test.add_account(
allocated_pubkey,
Account {
Expand Down
8 changes: 8 additions & 0 deletions test-pinocchio.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
PROGRAM_NAME="$1"
ROOT_DIR="$(cd "$(dirname "$0")"; pwd)"
#set -e
PROGRAM_DIR=$ROOT_DIR/$PROGRAM_NAME
cd $PROGRAM_DIR/pinocchio
cargo build-sbf
PROGRAM_NAME="pinocchio_rosetta_${PROGRAM_NAME//-/_}" SBF_OUT_DIR="$ROOT_DIR/target/deploy" cargo test --manifest-path "$PROGRAM_DIR/Cargo.toml"
13 changes: 13 additions & 0 deletions transfer-lamports/pinocchio/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "pinocchio-rosetta-transfer-lamports"
version = "1.0.0"
edition = "2021"

[features]
no-entrypoint = []

[dependencies]
pinocchio = "0.6"

[lib]
crate-type = ["cdylib", "lib"]
49 changes: 49 additions & 0 deletions transfer-lamports/pinocchio/src/entrypoint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//! Program entrypoint
#![cfg(not(feature = "no-entrypoint"))]

use pinocchio::{
lazy_entrypoint::{InstructionContext, MaybeAccount},
program_error::ProgramError,
ProgramResult,
};

// Since this is a single instruction program, we use the "lazy" variation
// of the entrypoint.
pinocchio::lazy_entrypoint!(process_instruction);

#[inline]
fn process_instruction(mut context: InstructionContext) -> ProgramResult {
if context.remaining() != 2 {
return Err(ProgramError::NotEnoughAccountKeys);
}

// This block is declared unsafe because:
//
// - We are using `next_account_unchecked`, which does not decrease the number of
// remaining accounts in the context. This is ok because we know that we have
// exactly two accounts.
//
// - We are using `assume_account` on the first account, which is ok because we
// know that we have at least one account.
//
// - We are using `borrow_mut_lamports_unchecked`, which is ok because we know
// that the lamports are not borrowed elsewhere and the accounts are different.
unsafe {
let source_info = context.next_account_unchecked().assume_account();

// The second account is the destination account – this one could be duplicated.
//
// We only need to transfer lamports from the source to the destination when the
// accounts are different, so we can safely ignore the case when the account is
// duplicated.
if let MaybeAccount::Account(destination_info) = context.next_account_unchecked() {
// withdraw five lamports
*source_info.borrow_mut_lamports_unchecked() -= 5;
// deposit five lamports
*destination_info.borrow_mut_lamports_unchecked() += 5;
}
}

Ok(())
}
4 changes: 4 additions & 0 deletions transfer-lamports/pinocchio/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//! A program demonstrating the transfer of lamports
#![deny(missing_docs)]

mod entrypoint;
9 changes: 7 additions & 2 deletions transfer-lamports/tests/functional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ async fn test_lamport_transfer() {
let program_id = Pubkey::from_str("TransferLamports111111111111111111111111111").unwrap();
let source_pubkey = Pubkey::new_unique();
let destination_pubkey = Pubkey::new_unique();
let mut program_test =
ProgramTest::new("solana_program_rosetta_transfer_lamports", program_id, None);

let mut program_test = ProgramTest::new(
option_env!("PROGRAM_NAME").unwrap_or("solana_program_rosetta_transfer_lamports"),
program_id,
None,
);

let source_lamports = 5;
let destination_lamports = 890_875;
program_test.add_account(
Expand Down

0 comments on commit 45d9595

Please sign in to comment.