Skip to content
Draft
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
17 changes: 16 additions & 1 deletion Cargo.lock

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

14 changes: 12 additions & 2 deletions interface/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,19 @@ repository = "https://github.com/solana-program/memo"
license = "Apache-2.0"
edition = "2021"

[features]
cpi = ["dep:solana-account-view", "dep:solana-instruction-view", "dep:solana-program-error"]
instruction = ["dep:solana-instruction"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

instruction as a feature might encourage the wrong behavior, since the cpi helpers also create instructions (in a way).

I can get behind instruction if it's preferred by others, but we could go with alloc to be technically correct, since that's what solana-instruction requires, or we can go with offchain to make it clearer where it should be used.

Of the options, I think I prefer alloc the most.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, the name is definitely not ideal, but I found it tricky to come up with a feature name for this. Maybe alloc would be best, although it does not directly reflect what is being enabled; offchain is tricky since anyone still using AccountInfo will need to use this feature to get an instruction for CPI.


[dependencies]
solana-instruction = "3.4.0"
solana-pubkey = "4.2.0"
solana-account-view = { version = "2.0", optional = true }
solana-address = { version = "2.6", features = ["decode"] }
solana-instruction = { version = "3.4.0", optional = true }
solana-instruction-view = { version = "2.1", features = ["cpi"], optional = true }
solana-program-error = { version = "3.0", optional = true }

[dev-dependencies]
solana-address = { version = "2.6", features = ["atomic"] }

[lib]
crate-type = ["lib"]
Expand Down
79 changes: 79 additions & 0 deletions interface/src/cpi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use {
core::{mem::MaybeUninit, slice::from_raw_parts},
solana_account_view::AccountView,
solana_address::Address,
solana_instruction_view::{
cpi::{invoke_signed_unchecked, CpiAccount, Signer, MAX_STATIC_CPI_ACCOUNTS},
InstructionAccount, InstructionView,
},
solana_program_error::{ProgramError, ProgramResult},
};

/// Writes a message to the transaction log, validating that
/// provided accounts are signers.
///
/// ### Accounts:
/// 0. `..+N` `[SIGNER]` N signing accounts
pub struct Memo<'memo, 'signers, S: AsRef<AccountView>> {
/// The message to log.
pub message: &'memo str,

/// Signing accounts.
pub signers: &'signers [S],

/// The Memo program to invoke.
pub program_id: &'static Address,
}

impl<S: AsRef<AccountView>> Memo<'_, '_, S> {
/// Invokes the Memo program with the provided message and signing accounts.
#[inline(always)]
pub fn invoke(&self) -> ProgramResult {
self.invoke_signed(&[])
}

/// Invokes the Memo program with the provided message and signing accounts.
///
/// Seeds for signing accounts can be provided via `signers` parameter.
#[inline(always)]
pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult {
let mut instruction_accounts =
[const { MaybeUninit::<InstructionAccount>::uninit() }; MAX_STATIC_CPI_ACCOUNTS];

let expected_account = self.signers.len();

if expected_account > MAX_STATIC_CPI_ACCOUNTS {
return Err(ProgramError::InvalidArgument);
}

let mut accounts = [const { MaybeUninit::<CpiAccount>::uninit() }; MAX_STATIC_CPI_ACCOUNTS];

for i in 0..expected_account {
// SAFETY: `expected_account` is less than MAX_STATIC_CPI_ACCOUNTS.
unsafe {
let signer = self.signers.get_unchecked(i);

instruction_accounts.get_unchecked_mut(i).write(
InstructionAccount::readonly_signer(signer.as_ref().address()),
);

CpiAccount::init_from_account_view(signer.as_ref(), accounts.get_unchecked_mut(i));
}
}

// SAFETY: both `instruction_accounts` and `accounts` are initialized.
unsafe {
invoke_signed_unchecked(
&InstructionView {
program_id: self.program_id,
accounts: from_raw_parts(instruction_accounts.as_ptr() as _, expected_account),
data: self.message.as_bytes(),
},
from_raw_parts(accounts.as_ptr() as _, expected_account),
signers,
);
}

Ok(())
}
}
21 changes: 11 additions & 10 deletions interface/src/instruction.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
use {
solana_address::Address,
solana_instruction::{AccountMeta, Instruction},
solana_pubkey::Pubkey,
};

/// Build a memo instruction, possibly signed
/// Build a memo instruction, possibly signed.
///
/// Accounts expected by this instruction:
///
/// 0. `..0+N` `[signer]` Expected signers; if zero provided, instruction will
/// be processed as a normal, unsigned spl-memo
pub fn build_memo(program_id: &Pubkey, memo: &[u8], signer_pubkeys: &[&Pubkey]) -> Instruction {
pub fn memo(program_id: &Address, message: &[u8], signer_pubkeys: &[&Address]) -> Instruction {
Instruction {
program_id: *program_id,
accounts: signer_pubkeys
.iter()
.map(|&pubkey| AccountMeta::new_readonly(*pubkey, true))
.collect(),
data: memo.to_vec(),
data: message.to_vec(),
}
}

Expand All @@ -25,12 +25,13 @@ mod tests {
use super::*;

#[test]
fn test_build_memo() {
let program_id = Pubkey::new_unique();
let signer_pubkey = Pubkey::new_unique();
let memo = "🐆".as_bytes();
let instruction = build_memo(&program_id, memo, &[&signer_pubkey]);
assert_eq!(memo, instruction.data);
fn test_memo() {
let program_id = Address::new_unique();
let signer_pubkey = Address::new_unique();
let message = "🐆".as_bytes();
let instruction = memo(&program_id, message, &[&signer_pubkey]);

assert_eq!(message, instruction.data);
assert_eq!(instruction.accounts.len(), 1);
assert_eq!(instruction.accounts[0].pubkey, signer_pubkey);
}
Expand Down
14 changes: 10 additions & 4 deletions interface/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
#![no_std]
#![deny(missing_docs)]

//! An interface for programs that accept a string of encoded characters and
//! verifies that it parses, while verifying and logging signers.

/// Instruction type
/// CPI interface for invoking the Memo program.
#[cfg(feature = "cpi")]
pub mod cpi;

/// Instruction definition for the Memo program.
#[cfg(feature = "instruction")]
pub mod instruction;

/// Legacy symbols from Memo version 1
pub mod v1 {
solana_pubkey::declare_id!("Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo");
solana_address::declare_id!("Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo");
}

/// Symbols from Memo version 3
pub mod v3 {
solana_pubkey::declare_id!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
solana_address::declare_id!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
}

/// Symbols from Memo version 4
pub mod v4 {
solana_pubkey::declare_id!("Memo4c2pN8afCj432Lb7RMVKi9PbQnnW7ewFFaV3oAH");
solana_address::declare_id!("Memo4c2pN8afCj432Lb7RMVKi9PbQnnW7ewFFaV3oAH");
}
2 changes: 1 addition & 1 deletion program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ solana-account = "3.4.0"
solana-instruction = "3.4.0"
solana-program-error = "3.0.1"
solana-pubkey = "4.2.0"
spl-memo-interface = { path = "../interface" }
spl-memo-interface = { path = "../interface", features = ["instruction"] }
40 changes: 18 additions & 22 deletions program/tests/functional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ use {
solana_instruction::{error::InstructionError, AccountMeta, Instruction},
solana_program_error::ProgramError,
solana_pubkey::Pubkey,
spl_memo_interface::{instruction::build_memo, v4::id},
spl_memo_interface::{instruction::memo, v4::id},
};

#[test]
fn test_memo_signing() {
let memo = "🐆".as_bytes();
let message = "🐆".as_bytes();
let mollusk = Mollusk::new(&id(), "pinocchio_memo_program");

let first_address = Pubkey::new_unique();
Expand All @@ -21,7 +21,7 @@ fn test_memo_signing() {
// Test complete signing
let signer_key_refs: Vec<&Pubkey> = pubkeys.iter().collect();
mollusk.process_and_validate_instruction(
&build_memo(&id(), memo, &signer_key_refs),
&memo(&id(), message, &signer_key_refs),
&[
(first_address, Account::default()),
(second_address, Account::default()),
Expand All @@ -31,11 +31,7 @@ fn test_memo_signing() {
);

// Test unsigned memo
mollusk.process_and_validate_instruction(
&build_memo(&id(), memo, &[]),
&[],
&[Check::success()],
);
mollusk.process_and_validate_instruction(&memo(&id(), message, &[]), &[], &[Check::success()]);

// Test missing signer(s)
mollusk.process_and_validate_instruction(
Expand All @@ -46,7 +42,7 @@ fn test_memo_signing() {
AccountMeta::new_readonly(second_address, false),
AccountMeta::new_readonly(third_address, true),
],
data: memo.to_vec(),
data: message.to_vec(),
},
&[
(first_address, Account::default()),
Expand All @@ -64,7 +60,7 @@ fn test_memo_signing() {
AccountMeta::new_readonly(second_address, false),
AccountMeta::new_readonly(third_address, false),
],
data: memo.to_vec(),
data: message.to_vec(),
},
&[
(first_address, Account::default()),
Expand All @@ -77,7 +73,7 @@ fn test_memo_signing() {
// Test invalid utf-8; demonstrate log
let invalid_utf8 = [0xF0, 0x9F, 0x90, 0x86, 0xF0, 0x9F, 0xFF, 0x86];
mollusk.process_and_validate_instruction(
&build_memo(&id(), &invalid_utf8, &[]),
&memo(&id(), &invalid_utf8, &[]),
&[],
&[Check::instruction_err(
InstructionError::ProgramFailedToComplete,
Expand All @@ -90,52 +86,52 @@ fn test_memo_compute_limits() {
let mollusk = Mollusk::new(&id(), "pinocchio_memo_program");

// Test memo length
let mut memo = vec![];
let mut message = vec![];
for _ in 0..1000 {
let mut vec = vec![0x53, 0x4F, 0x4C];
memo.append(&mut vec);
message.append(&mut vec);
}

mollusk.process_and_validate_instruction(
&build_memo(&id(), &memo[..450], &[]),
&memo(&id(), &message[..450], &[]),
&[],
&[Check::success()],
);

mollusk.process_and_validate_instruction(
&build_memo(&id(), &memo[..600], &[]),
&memo(&id(), &message[..600], &[]),
&[],
&[Check::success(), Check::compute_units(800)],
);

let mut memo = vec![];
let mut message = vec![];
for _ in 0..100 {
let mut vec = vec![0xE2, 0x97, 0x8E];
memo.append(&mut vec);
message.append(&mut vec);
}

mollusk.process_and_validate_instruction(
&build_memo(&id(), &memo[..60], &[]),
&memo(&id(), &message[..60], &[]),
&[],
&[Check::success()],
);

mollusk.process_and_validate_instruction(
&build_memo(&id(), &memo[..63], &[]),
&memo(&id(), &message[..63], &[]),
&[],
&[Check::success(), Check::compute_units(287)],
);

// Test num signers with 32-byte memo
let memo = [b'1'; 32];
let message = [b'1'; 32];
let mut pubkeys = vec![];
for _ in 0..20 {
pubkeys.push(Pubkey::new_unique());
}
let signer_key_refs: Vec<&Pubkey> = pubkeys.iter().collect();

mollusk.process_and_validate_instruction(
&build_memo(&id(), &memo, &signer_key_refs[..12]),
&memo(&id(), &message, &signer_key_refs[..12]),
pubkeys
.iter()
.take(12)
Expand All @@ -146,7 +142,7 @@ fn test_memo_compute_limits() {
);

mollusk.process_and_validate_instruction(
&build_memo(&id(), &memo, &signer_key_refs[..15]),
&memo(&id(), &message, &signer_key_refs[..15]),
pubkeys
.iter()
.take(15)
Expand Down