Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Rust programs using pinocchio #7

Merged
merged 11 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from 10 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
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());
febo marked this conversation as resolved.
Show resolved Hide resolved

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(())
}
18 changes: 15 additions & 3 deletions cpi/tests/functional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,34 @@ 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,
};

/// The name of the program to test when using the `solana_program` library.
const SOLANA_PROGRAM: &str = "solana_program_rosetta_cpi";

/// The name of the program to test when using the `pinocchio` library.
const PINOCCHIO_PROGRAM: &str = "pinocchio_rosetta_cpi";

#[tokio::test]
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 library = std::env::var("ROSETTA_LIBRARY").unwrap_or(String::from("solana_program"));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needing to specify the ROSETTA_LIBRARY env variable and also hard-code it into the test seems a bit overkill -- how about having an environment variable like PROGRAM_NAME and just using that or the default?

Then test-pinocchio.sh just needs to combine pinocchio with the program name. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, but we still have the issue that ProgramTest::new expects the program_name as a &'static str. That is the only reason for hard-coding the name on the test file.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about using env! in that case? https://doc.rust-lang.org/std/macro.env.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea - ended up using option_env! so we can specify a default value if the variable is not set.

let mut program_test = ProgramTest::new(
"solana_program_rosetta_cpi",
match library.as_str() {
"pinocchio" => PINOCCHIO_PROGRAM,
_ => SOLANA_PROGRAM,
},
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
ROSETTA_LIBRARY="pinocchio" 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;
19 changes: 17 additions & 2 deletions transfer-lamports/tests/functional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,28 @@ use {
std::str::FromStr,
};

/// The name of the program to test when using the `solana_program` library.
const SOLANA_PROGRAM: &str = "solana_program_rosetta_transfer_lamports";

/// The name of the program to test when using the `pinocchio` library.
const PINOCCHIO_PROGRAM: &str = "pinocchio_rosetta_transfer_lamports";

#[tokio::test]
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 library = std::env::var("ROSETTA_LIBRARY").unwrap_or(String::from("solana_program"));
febo marked this conversation as resolved.
Show resolved Hide resolved
let mut program_test = ProgramTest::new(
match library.as_str() {
"pinocchio" => PINOCCHIO_PROGRAM,
_ => SOLANA_PROGRAM,
},
program_id,
None,
);

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