Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"private": true,
"scripts": {
"generate:clients": "zx ./scripts/generate-clients.mjs"
"generate:clients": "zx ./scripts/generate-clients.mjs",
"programs:build": "cargo build-sbf --manifest-path program/Cargo.toml && cargo build-sbf --manifest-path pinocchio/Cargo.toml",
"programs:test": "cargo test-sbf --manifest-path program/Cargo.toml && SBF_OUT_DIR=../target/deploy cargo test --manifest-path pinocchio/Cargo.toml",
"programs:lint": "cargo clippy --manifest-path program/Cargo.toml && cargo clippy --manifest-path pinocchio/Cargo.toml"
},
"devDependencies": {
"@codama/renderers-js": "^1.5",
Expand Down
31 changes: 17 additions & 14 deletions pinocchio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,55 @@ A `pinocchio`-based Memo program.

## Overview

`p-memo` is a reimplementation of the SPL Memo program using [`pinocchio`](https://github.com/anza-xyz/pinocchio). The program uses at most `~5%` of the compute units used by the current Memo program when signers are present; even when there are no signers, it needs only `~20%` of the current Memo program compute units. This efficiency is achieved by a combination of:
`p-memo` is a reimplementation of the SPL Memo program using [`pinocchio`](https://github.com/anza-xyz/pinocchio). The program uses at most `~5%` of the compute units used by the current Memo program when signers are present; even when there are no signers, it needs only `~14%` of the current Memo program compute units. This efficiency is achieved by a combination of:

1. `pinocchio` "lazy" entrypoint
2. `sol_log_pubkey` syscall to log pubkey values
3. [`pinocchio-log`](https://crates.io/crates/pinocchio-log) to format the memo message
3. Direct `sol_log_` syscalls for efficient logging

Since it uses the syscall to log pubkeys, the output of the program is slightly different while loging the same information:

```
1: Program PMemo11111111111111111111111111111111111111 invoke [1]
2: Program log: Signed by:
3: Program log: 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM
4: Program log: Memo (len 60):
5: Program log: why does spl memo use 36000 cus to print len 60 msg of ascii
6: Program PMemo11111111111111111111111111111111111111 consumed 537 of 1400000 compute units
7: Program PMemo11111111111111111111111111111111111111 success
4: Program log: why does spl memo use 36000 cus to print len 60 msg of ascii
5: Program PMemo11111111111111111111111111111111111111 consumed 537 of 1400000 compute units
6: Program PMemo11111111111111111111111111111111111111 success
```

Logging begins with entry into the program (`line 1`). Then there is a separate log to start the signers section (`line 2`); this is only present if there are signer accounts. After that there will be one line for each signer account (`line 3`), followed by the memo length and UTF-8 text (`line 4-5`). The program ends with the status of the instruction (`lines 6-7`).
Logging begins with entry into the program (`line 1`). Then there is a separate log to start the signers section (`line 2`); this is only present if there are signer accounts. After that there will be one line for each signer account (`line 3`), followed by the memo UTF-8 text (`line 4`). The program ends with the status of the instruction (`lines 5-6`).

## Performance

CU comsumption:

| \# signers | p-memo | SPL Memo |
| ---------- | ----------- | --------- |
| 0 | 313 (`15%`) | 2,022 |
| 1 | 537 (`4%`) | 13,525 |
| 2 | 654 (`3%`) | 25,111 |
| 3 | 771 (`2%`) | 36,406 |
| \# signers | p-memo | SPL Memo |
| ---------- | ----------- | -------- |
| 0 | 283 (`14%`) | 2,022 |
| 1 | 537 (`4%`) | 13,525 |
| 2 | 654 (`3%`) | 25,111 |
| 3 | 771 (`2%`) | 36,406 |

> [!NOTE]
> Using Solana CLI `v2.2.15`.

## Building

To build the program from its directory:

```bash
cargo build-sbf
```

## Testing

To run the tests (after building the program):

```bash
SBF_OUT_DIR=../target/deploy cargo test
```

## License

The code is licensed under the [Apache License Version 2.0](LICENSE)
The code is licensed under the [Apache License Version 2.0](LICENSE)
19 changes: 9 additions & 10 deletions pinocchio/src/entrypoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use pinocchio::{
syscalls::{sol_log_, sol_log_pubkey},
ProgramResult,
};
use pinocchio_log::log;

/// Process a memo instruction.
///
Expand All @@ -15,11 +14,12 @@ use pinocchio_log::log;
pub fn process_instruction(mut context: InstructionContext) -> ProgramResult {
let mut missing_required_signature = false;

// Validates signer accounts (if any).

if context.remaining() > 0 {
// Logs a message indicating that there are signers.
log!("Signed by:");
const SIGNED_BY_MSG: &[u8] = b"Signed by:";
// SAFETY: Logging a constant string with known valid length.
unsafe {
sol_log_(SIGNED_BY_MSG.as_ptr(), SIGNED_BY_MSG.len() as u64);
}
Comment on lines +18 to +22
Copy link
Contributor

Choose a reason for hiding this comment

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

The macro will expand to exactly this, so no need to make the change


while context.remaining() > 0 {
// Duplicated accounts are implicitly checked since at least one of the
Expand All @@ -39,13 +39,12 @@ pub fn process_instruction(mut context: InstructionContext) -> ProgramResult {
}
}

// SAFETY: All accounts have been processed.
let instruction_data = unsafe { context.instruction_data_unchecked() };
let instruction_data = context.instruction_data()?;

// Logs the length of the memo message and its content.
let _memo =
core::str::from_utf8(instruction_data).map_err(|_| ProgramError::InvalidInstructionData)?;
Comment on lines +44 to +45
Copy link
Contributor

Choose a reason for hiding this comment

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

the utf8 validation is performed by the syscall, which is why the program doesn't handle it


log!("Memo (len {}):", instruction_data.len());
Copy link
Contributor

Choose a reason for hiding this comment

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

Removing this log is probably the real source of CU gains from this change

// SAFETY: The syscall will validate the UTF-8 encoding of the memo data.
// SAFETY: UTF-8 validation passed, syscall will succeed.
unsafe {
sol_log_(instruction_data.as_ptr(), instruction_data.len() as u64);
}
Expand Down
42 changes: 38 additions & 4 deletions pinocchio/tests/memo.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use mollusk_svm::{result::Check, Mollusk};
use solana_account::Account;
use solana_instruction::{error::InstructionError, AccountMeta, Instruction};
use solana_instruction::{AccountMeta, Instruction};
use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;

Expand Down Expand Up @@ -47,9 +47,7 @@ fn fail_test_invalid_ascii_no_accounts() {
mollusk.process_and_validate_instruction(
&instruction,
&[],
&[Check::instruction_err(
InstructionError::ProgramFailedToComplete,
)],
&[Check::err(ProgramError::InvalidInstructionData)],
);
}

Expand Down Expand Up @@ -138,3 +136,39 @@ fn test_valid_ascii_duplicated_accounts() {
&[Check::success()],
);
}

#[test]
fn test_valid_empty_memo() {
let mollusk = Mollusk::new(&PROGRAM_ID, "p_memo");

let instruction = instruction(&[], None);

mollusk.process_and_validate_instruction(&instruction, &[], &[Check::success()]);
}

#[test]
fn test_valid_max_size_memo() {
let mollusk = Mollusk::new(&PROGRAM_ID, "p_memo");

// Maximum transaction MTU is 1232 bytes.
let max_memo = vec![b'A'; 1232];
let instruction = instruction(&max_memo, None);

mollusk.process_and_validate_instruction(&instruction, &[], &[Check::success()]);
}

#[test]
fn test_invalid_utf8_error_code() {
let mollusk = Mollusk::new(&PROGRAM_ID, "p_memo");

// Invalid UTF-8 sequence.
let invalid_utf8 = [0xF0, 0x9F, 0xFF, 0x86];
let instruction = instruction(&invalid_utf8, None);

// Verify that we return InvalidInstructionData, not ProgramFailedToComplete.
mollusk.process_and_validate_instruction(
&instruction,
&[],
&[Check::err(ProgramError::InvalidInstructionData)],
);
}