Skip to content

Commit 45d9595

Browse files
authored
Add Rust programs using pinocchio (#7)
* 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
1 parent 8fe1bf0 commit 45d9595

File tree

12 files changed

+256
-6
lines changed

12 files changed

+256
-6
lines changed

.github/workflows/main.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,36 @@ jobs:
150150

151151
- name: Build and test program
152152
run: ./test-asm.sh ${{ matrix.program }}
153+
154+
pinocchio-test:
155+
name: Run tests against Pinocchio Rust implementations
156+
strategy:
157+
matrix:
158+
program: [transfer-lamports, cpi]
159+
fail-fast: false
160+
runs-on: ubuntu-latest
161+
steps:
162+
- uses: actions/checkout@v4
163+
- uses: actions/cache@v4
164+
with:
165+
path: |
166+
~/.cargo/registry
167+
~/.cargo/git
168+
~/.cache/solana
169+
key: rust-${{ hashFiles('./Cargo.lock') }}
170+
171+
- name: Install Rust
172+
uses: dtolnay/rust-toolchain@master
173+
with:
174+
toolchain: 1.78.0
175+
176+
- name: Install Rust build deps
177+
run: ./install-rust-build-deps.sh
178+
179+
- name: Install Solana
180+
run: |
181+
./install-solana.sh
182+
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
183+
184+
- name: Build and test program
185+
run: ./test-pinocchio.sh ${{ matrix.program }}

Cargo.lock

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
[workspace]
22
members = [
33
"cpi",
4+
"cpi/pinocchio",
45
"helloworld",
5-
"transfer-lamports"
6+
"transfer-lamports",
7+
"transfer-lamports/pinocchio"
68
]
79
resolver = "2"
810

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ lets the VM assume it worked.
165165
| Zig | 43 |
166166
| C | 103 |
167167
| Assembly | 22 |
168+
| Rust (pinocchio) | 23 |
168169

169170
This one starts to get interesting since it requires parsing the instruction
170171
input. Since the assembly version knows exactly where to find everything, it can
@@ -182,3 +183,4 @@ the address and `invoke_signed` to CPI to the system program.
182183
| Rust | 3662 |
183184
| Zig | 2825 |
184185
| C | 3122 |
186+
| Rust (pinocchio) | 2816 |

cpi/pinocchio/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "pinocchio-rosetta-cpi"
3+
version = "1.0.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
pinocchio = "0.6"
8+
pinocchio-system = "0.2"
9+
10+
[lib]
11+
crate-type = ["cdylib", "lib"]

cpi/pinocchio/src/lib.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//! Rust example using pinocchio demonstrating invoking another program
2+
#![deny(missing_docs)]
3+
4+
use pinocchio::{
5+
instruction::{Account, AccountMeta, Instruction},
6+
lazy_entrypoint::InstructionContext,
7+
program::invoke_signed_unchecked,
8+
program_error::ProgramError,
9+
pubkey::create_program_address,
10+
signer, ProgramResult,
11+
};
12+
13+
// Since this is a single instruction program, we use the "lazy" variation
14+
// of the entrypoint.
15+
pinocchio::lazy_entrypoint!(process_instruction);
16+
17+
/// Amount of bytes of account data to allocate
18+
pub const SIZE: usize = 42;
19+
20+
/// Instruction processor.
21+
fn process_instruction(mut context: InstructionContext) -> ProgramResult {
22+
if context.remaining() != 2 {
23+
return Err(ProgramError::NotEnoughAccountKeys);
24+
}
25+
26+
// Account info to allocate and for the program being invoked. We know that
27+
// we got 2 accounts, so it is ok use `next_account_unchecked` twice.
28+
let allocated_info = unsafe { context.next_account_unchecked().assume_account() };
29+
// just move the offset, we don't need the system program info
30+
let _system_program_info = unsafe { context.next_account_unchecked() };
31+
32+
// Again, don't need to check that all accounts have been consumed, we know
33+
// we have exactly 2 accounts.
34+
let (instruction_data, program_id) = unsafe { context.instruction_data_unchecked() };
35+
36+
let expected_allocated_key =
37+
create_program_address(&[b"You pass butter", &[instruction_data[0]]], program_id)?;
38+
if *allocated_info.key() != expected_allocated_key {
39+
// allocated key does not match the derived address
40+
return Err(ProgramError::InvalidArgument);
41+
}
42+
43+
// Invoke the system program to allocate account data
44+
let mut data = [0; 12];
45+
data[0] = 8; // ix discriminator
46+
data[4..12].copy_from_slice(&SIZE.to_le_bytes());
47+
48+
let instruction = Instruction {
49+
program_id: &pinocchio_system::ID,
50+
accounts: &[AccountMeta::writable_signer(allocated_info.key())],
51+
data: &data,
52+
};
53+
54+
// Invoke the system program with the 'unchecked' function - this is ok since
55+
// we know the accounts are not borrowed elsewhere.
56+
unsafe {
57+
invoke_signed_unchecked(
58+
&instruction,
59+
&[Account::from(&allocated_info)],
60+
&[signer!(b"You pass butter", &[instruction_data[0]])],
61+
)
62+
};
63+
64+
Ok(())
65+
}

cpi/tests/functional.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use {
55
rent::Rent,
66
system_program,
77
},
8-
solana_program_rosetta_cpi::{process_instruction, SIZE},
8+
solana_program_rosetta_cpi::SIZE,
99
solana_program_test::*,
1010
solana_sdk::{account::Account, signature::Signer, transaction::Transaction},
1111
std::str::FromStr,
@@ -16,11 +16,13 @@ async fn test_cross_program_invocation() {
1616
let program_id = Pubkey::from_str("invoker111111111111111111111111111111111111").unwrap();
1717
let (allocated_pubkey, bump_seed) =
1818
Pubkey::find_program_address(&[b"You pass butter"], &program_id);
19+
1920
let mut program_test = ProgramTest::new(
20-
"solana_program_rosetta_cpi",
21+
option_env!("PROGRAM_NAME").unwrap_or("solana_program_rosetta_cpi"),
2122
program_id,
22-
processor!(process_instruction),
23+
None,
2324
);
25+
2426
program_test.add_account(
2527
allocated_pubkey,
2628
Account {

test-pinocchio.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bash
2+
PROGRAM_NAME="$1"
3+
ROOT_DIR="$(cd "$(dirname "$0")"; pwd)"
4+
#set -e
5+
PROGRAM_DIR=$ROOT_DIR/$PROGRAM_NAME
6+
cd $PROGRAM_DIR/pinocchio
7+
cargo build-sbf
8+
PROGRAM_NAME="pinocchio_rosetta_${PROGRAM_NAME//-/_}" SBF_OUT_DIR="$ROOT_DIR/target/deploy" cargo test --manifest-path "$PROGRAM_DIR/Cargo.toml"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "pinocchio-rosetta-transfer-lamports"
3+
version = "1.0.0"
4+
edition = "2021"
5+
6+
[features]
7+
no-entrypoint = []
8+
9+
[dependencies]
10+
pinocchio = "0.6"
11+
12+
[lib]
13+
crate-type = ["cdylib", "lib"]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//! Program entrypoint
2+
3+
#![cfg(not(feature = "no-entrypoint"))]
4+
5+
use pinocchio::{
6+
lazy_entrypoint::{InstructionContext, MaybeAccount},
7+
program_error::ProgramError,
8+
ProgramResult,
9+
};
10+
11+
// Since this is a single instruction program, we use the "lazy" variation
12+
// of the entrypoint.
13+
pinocchio::lazy_entrypoint!(process_instruction);
14+
15+
#[inline]
16+
fn process_instruction(mut context: InstructionContext) -> ProgramResult {
17+
if context.remaining() != 2 {
18+
return Err(ProgramError::NotEnoughAccountKeys);
19+
}
20+
21+
// This block is declared unsafe because:
22+
//
23+
// - We are using `next_account_unchecked`, which does not decrease the number of
24+
// remaining accounts in the context. This is ok because we know that we have
25+
// exactly two accounts.
26+
//
27+
// - We are using `assume_account` on the first account, which is ok because we
28+
// know that we have at least one account.
29+
//
30+
// - We are using `borrow_mut_lamports_unchecked`, which is ok because we know
31+
// that the lamports are not borrowed elsewhere and the accounts are different.
32+
unsafe {
33+
let source_info = context.next_account_unchecked().assume_account();
34+
35+
// The second account is the destination account – this one could be duplicated.
36+
//
37+
// We only need to transfer lamports from the source to the destination when the
38+
// accounts are different, so we can safely ignore the case when the account is
39+
// duplicated.
40+
if let MaybeAccount::Account(destination_info) = context.next_account_unchecked() {
41+
// withdraw five lamports
42+
*source_info.borrow_mut_lamports_unchecked() -= 5;
43+
// deposit five lamports
44+
*destination_info.borrow_mut_lamports_unchecked() += 5;
45+
}
46+
}
47+
48+
Ok(())
49+
}

0 commit comments

Comments
 (0)