diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9b1d06c..03e937b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 }} diff --git a/Cargo.lock b/Cargo.lock index d332008..4496581 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1311,6 +1311,21 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "five8_const" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b4f62f0f8ca357f93ae90c8c2dd1041a1f665fde2f889ea9b1787903829015" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a72055cd9cffc40c9f75f1e5810c80559e158796cf2202292ce4745889588" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -2516,6 +2531,47 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pinocchio" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9f716de2190437efa787dd7414f4bcea88d22c2f81bbeecabb7db6d9cc326bd" + +[[package]] +name = "pinocchio-pubkey" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4800103b9bea24df2e78f161403fc8f8c2286cb5f874b2e5feebe6c5c4fb35a" +dependencies = [ + "five8_const", + "pinocchio", +] + +[[package]] +name = "pinocchio-rosetta-cpi" +version = "1.0.0" +dependencies = [ + "pinocchio", + "pinocchio-system", +] + +[[package]] +name = "pinocchio-rosetta-transfer-lamports" +version = "1.0.0" +dependencies = [ + "pinocchio", +] + +[[package]] +name = "pinocchio-system" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a50555b55fde54156049064cc503219068ee53ecd4add7014ec928ee21f67f" +dependencies = [ + "pinocchio", + "pinocchio-pubkey", +] + [[package]] name = "pkg-config" version = "0.3.30" diff --git a/Cargo.toml b/Cargo.toml index 17fc4a7..22181dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,10 @@ [workspace] members = [ "cpi", + "cpi/pinocchio", "helloworld", - "transfer-lamports" + "transfer-lamports", + "transfer-lamports/pinocchio" ] resolver = "2" diff --git a/README.md b/README.md index 11dbf2b..4097c46 100644 --- a/README.md +++ b/README.md @@ -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 @@ -182,3 +183,4 @@ the address and `invoke_signed` to CPI to the system program. | Rust | 3662 | | Zig | 2825 | | C | 3122 | +| Rust (pinocchio) | 2816 | diff --git a/cpi/pinocchio/Cargo.toml b/cpi/pinocchio/Cargo.toml new file mode 100644 index 0000000..40993a5 --- /dev/null +++ b/cpi/pinocchio/Cargo.toml @@ -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"] diff --git a/cpi/pinocchio/src/lib.rs b/cpi/pinocchio/src/lib.rs new file mode 100644 index 0000000..581c358 --- /dev/null +++ b/cpi/pinocchio/src/lib.rs @@ -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(()) +} diff --git a/cpi/tests/functional.rs b/cpi/tests/functional.rs index c8b3413..72282cf 100644 --- a/cpi/tests/functional.rs +++ b/cpi/tests/functional.rs @@ -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, @@ -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 { diff --git a/test-pinocchio.sh b/test-pinocchio.sh new file mode 100755 index 0000000..269d374 --- /dev/null +++ b/test-pinocchio.sh @@ -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" \ No newline at end of file diff --git a/transfer-lamports/pinocchio/Cargo.toml b/transfer-lamports/pinocchio/Cargo.toml new file mode 100644 index 0000000..7803d45 --- /dev/null +++ b/transfer-lamports/pinocchio/Cargo.toml @@ -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"] diff --git a/transfer-lamports/pinocchio/src/entrypoint.rs b/transfer-lamports/pinocchio/src/entrypoint.rs new file mode 100644 index 0000000..91b20cf --- /dev/null +++ b/transfer-lamports/pinocchio/src/entrypoint.rs @@ -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(()) +} diff --git a/transfer-lamports/pinocchio/src/lib.rs b/transfer-lamports/pinocchio/src/lib.rs new file mode 100644 index 0000000..4f5bd25 --- /dev/null +++ b/transfer-lamports/pinocchio/src/lib.rs @@ -0,0 +1,4 @@ +//! A program demonstrating the transfer of lamports +#![deny(missing_docs)] + +mod entrypoint; diff --git a/transfer-lamports/tests/functional.rs b/transfer-lamports/tests/functional.rs index ee94c08..a090436 100644 --- a/transfer-lamports/tests/functional.rs +++ b/transfer-lamports/tests/functional.rs @@ -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(