diff --git a/program/Cargo.toml b/program/Cargo.toml new file mode 100644 index 00000000..7631d51b --- /dev/null +++ b/program/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "spl-memo" +version = "6.0.0" +description = "Solana Program Library Memo" +authors = ["Solana Labs Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2021" + +[features] +no-entrypoint = [] +test-sbf = [] + +[dependencies] +solana-account-info = "2.1.0" +solana-instruction = "2.1.0" +solana-msg = "2.1.0" +solana-program-entrypoint = "2.1.0" +solana-program-error = "2.1.0" +solana-pubkey = "2.1.0" + +[dev-dependencies] +solana-program-test = "2.1.0" +solana-sdk = "2.1.0" + +[lib] +crate-type = ["cdylib", "lib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/program/src/entrypoint.rs b/program/src/entrypoint.rs new file mode 100644 index 00000000..09ae9409 --- /dev/null +++ b/program/src/entrypoint.rs @@ -0,0 +1,17 @@ +//! Program entrypoint + +#![cfg(not(feature = "no-entrypoint"))] + +use { + solana_account_info::AccountInfo, solana_program_entrypoint::ProgramResult, + solana_pubkey::Pubkey, +}; + +solana_program_entrypoint::entrypoint!(process_instruction); +fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + crate::processor::process_instruction(program_id, accounts, instruction_data) +} diff --git a/program/src/lib.rs b/program/src/lib.rs new file mode 100644 index 00000000..898167a7 --- /dev/null +++ b/program/src/lib.rs @@ -0,0 +1,58 @@ +#![deny(missing_docs)] + +//! A program that accepts a string of encoded characters and verifies that it +//! parses, while verifying and logging signers. Currently handles UTF-8 +//! characters. + +mod entrypoint; +pub mod processor; + +// Export current sdk types for downstream users building with a different sdk +// version +pub use { + solana_account_info, solana_instruction, solana_msg, solana_program_entrypoint, + solana_program_error, solana_pubkey, +}; +use { + solana_instruction::{AccountMeta, Instruction}, + solana_pubkey::Pubkey, +}; + +/// Legacy symbols from Memo v1 +pub mod v1 { + solana_pubkey::declare_id!("Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo"); +} + +solana_pubkey::declare_id!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"); + +/// 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(memo: &[u8], signer_pubkeys: &[&Pubkey]) -> Instruction { + Instruction { + program_id: id(), + accounts: signer_pubkeys + .iter() + .map(|&pubkey| AccountMeta::new_readonly(*pubkey, true)) + .collect(), + data: memo.to_vec(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_memo() { + let signer_pubkey = Pubkey::new_unique(); + let memo = "🐆".as_bytes(); + let instruction = build_memo(memo, &[&signer_pubkey]); + assert_eq!(memo, instruction.data); + assert_eq!(instruction.accounts.len(), 1); + assert_eq!(instruction.accounts[0].pubkey, signer_pubkey); + } +} diff --git a/program/src/processor.rs b/program/src/processor.rs new file mode 100644 index 00000000..618290ad --- /dev/null +++ b/program/src/processor.rs @@ -0,0 +1,107 @@ +//! Program state processor + +use { + solana_account_info::AccountInfo, solana_msg::msg, solana_program_entrypoint::ProgramResult, + solana_program_error::ProgramError, solana_pubkey::Pubkey, std::str::from_utf8, +}; + +/// Instruction processor +pub fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + input: &[u8], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mut missing_required_signature = false; + for account_info in account_info_iter { + if let Some(address) = account_info.signer_key() { + msg!("Signed by {:?}", address); + } else { + missing_required_signature = true; + } + } + if missing_required_signature { + return Err(ProgramError::MissingRequiredSignature); + } + + let memo = from_utf8(input).map_err(|err| { + msg!("Invalid UTF-8, from byte {}", err.valid_up_to()); + ProgramError::InvalidInstructionData + })?; + msg!("Memo (len {}): {:?}", memo.len(), memo); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use { + super::*, solana_account_info::IntoAccountInfo, solana_program_error::ProgramError, + solana_pubkey::Pubkey, solana_sdk::account::Account, + }; + + #[test] + fn test_utf8_memo() { + let program_id = Pubkey::new_from_array([0; 32]); + + let string = b"letters and such"; + assert_eq!(Ok(()), process_instruction(&program_id, &[], string)); + + let emoji = "🐆".as_bytes(); + let bytes = [0xF0, 0x9F, 0x90, 0x86]; + assert_eq!(emoji, bytes); + assert_eq!(Ok(()), process_instruction(&program_id, &[], emoji)); + + let mut bad_utf8 = bytes; + bad_utf8[3] = 0xFF; // Invalid UTF-8 byte + assert_eq!( + Err(ProgramError::InvalidInstructionData), + process_instruction(&program_id, &[], &bad_utf8) + ); + } + + #[test] + fn test_signers() { + let program_id = Pubkey::new_from_array([0; 32]); + let memo = "🐆".as_bytes(); + + let pubkey0 = Pubkey::new_unique(); + let pubkey1 = Pubkey::new_unique(); + let pubkey2 = Pubkey::new_unique(); + let mut account0 = Account::default(); + let mut account1 = Account::default(); + let mut account2 = Account::default(); + + let signed_account_infos = vec![ + (&pubkey0, true, &mut account0).into_account_info(), + (&pubkey1, true, &mut account1).into_account_info(), + (&pubkey2, true, &mut account2).into_account_info(), + ]; + assert_eq!( + Ok(()), + process_instruction(&program_id, &signed_account_infos, memo) + ); + + assert_eq!(Ok(()), process_instruction(&program_id, &[], memo)); + + let unsigned_account_infos = vec![ + (&pubkey0, false, &mut account0).into_account_info(), + (&pubkey1, false, &mut account1).into_account_info(), + (&pubkey2, false, &mut account2).into_account_info(), + ]; + assert_eq!( + Err(ProgramError::MissingRequiredSignature), + process_instruction(&program_id, &unsigned_account_infos, memo) + ); + + let partially_signed_account_infos = vec![ + (&pubkey0, true, &mut account0).into_account_info(), + (&pubkey1, false, &mut account1).into_account_info(), + (&pubkey2, true, &mut account2).into_account_info(), + ]; + assert_eq!( + Err(ProgramError::MissingRequiredSignature), + process_instruction(&program_id, &partially_signed_account_infos, memo) + ); + } +} diff --git a/program/tests/functional.rs b/program/tests/functional.rs new file mode 100644 index 00000000..c8a05e1b --- /dev/null +++ b/program/tests/functional.rs @@ -0,0 +1,207 @@ +#![cfg(feature = "test-sbf")] + +use { + solana_instruction::{error::InstructionError, AccountMeta, Instruction}, + solana_program_test::*, + solana_pubkey::Pubkey, + solana_sdk::{ + signature::{Keypair, Signer}, + transaction::{Transaction, TransactionError}, + }, + spl_memo::*, +}; + +fn program_test() -> ProgramTest { + ProgramTest::new("spl_memo", id(), processor!(processor::process_instruction)) +} + +#[tokio::test] +async fn test_memo_signing() { + let memo = "🐆".as_bytes(); + let (banks_client, payer, recent_blockhash) = program_test().start().await; + + let keypairs = vec![Keypair::new(), Keypair::new(), Keypair::new()]; + let pubkeys: Vec = keypairs.iter().map(|keypair| keypair.pubkey()).collect(); + + // Test complete signing + let signer_key_refs: Vec<&Pubkey> = pubkeys.iter().collect(); + let mut transaction = + Transaction::new_with_payer(&[build_memo(memo, &signer_key_refs)], Some(&payer.pubkey())); + let mut signers = vec![&payer]; + for keypair in keypairs.iter() { + signers.push(keypair); + } + transaction.sign(&signers, recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + + // Test unsigned memo + let mut transaction = + Transaction::new_with_payer(&[build_memo(memo, &[])], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + + // Demonstrate success on signature provided, regardless of specific memo + // AccountMeta + let mut transaction = Transaction::new_with_payer( + &[Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new_readonly(keypairs[0].pubkey(), true), + AccountMeta::new_readonly(keypairs[1].pubkey(), true), + AccountMeta::new_readonly(payer.pubkey(), false), + ], + data: memo.to_vec(), + }], + Some(&payer.pubkey()), + ); + transaction.sign(&[&payer, &keypairs[0], &keypairs[1]], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + + // Test missing signer(s) + let mut transaction = Transaction::new_with_payer( + &[Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new_readonly(keypairs[0].pubkey(), true), + AccountMeta::new_readonly(keypairs[1].pubkey(), false), + AccountMeta::new_readonly(keypairs[2].pubkey(), true), + ], + data: memo.to_vec(), + }], + Some(&payer.pubkey()), + ); + transaction.sign(&[&payer, &keypairs[0], &keypairs[2]], recent_blockhash); + assert_eq!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ); + + let mut transaction = Transaction::new_with_payer( + &[Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new_readonly(keypairs[0].pubkey(), false), + AccountMeta::new_readonly(keypairs[1].pubkey(), false), + AccountMeta::new_readonly(keypairs[2].pubkey(), false), + ], + data: memo.to_vec(), + }], + Some(&payer.pubkey()), + ); + transaction.sign(&[&payer], recent_blockhash); + assert_eq!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ); + + // Test invalid utf-8; demonstrate log + let invalid_utf8 = [0xF0, 0x9F, 0x90, 0x86, 0xF0, 0x9F, 0xFF, 0x86]; + let mut transaction = + Transaction::new_with_payer(&[build_memo(&invalid_utf8, &[])], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + assert_eq!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData) + ); +} + +#[tokio::test] +#[ignore] +async fn test_memo_compute_limits() { + let (banks_client, payer, recent_blockhash) = program_test().start().await; + + // Test memo length + let mut memo = vec![]; + for _ in 0..1000 { + let mut vec = vec![0x53, 0x4F, 0x4C]; + memo.append(&mut vec); + } + + let mut transaction = + Transaction::new_with_payer(&[build_memo(&memo[..450], &[])], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + + let mut transaction = + Transaction::new_with_payer(&[build_memo(&memo[..600], &[])], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + let err = banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + let failed_to_complete = + TransactionError::InstructionError(0, InstructionError::ProgramFailedToComplete); + let computational_budget_exceeded = + TransactionError::InstructionError(0, InstructionError::ComputationalBudgetExceeded); + assert!(err == failed_to_complete || err == computational_budget_exceeded); + + let mut memo = vec![]; + for _ in 0..100 { + let mut vec = vec![0xE2, 0x97, 0x8E]; + memo.append(&mut vec); + } + + let mut transaction = + Transaction::new_with_payer(&[build_memo(&memo[..60], &[])], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + + let mut transaction = + Transaction::new_with_payer(&[build_memo(&memo[..63], &[])], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + let err = banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert!(err == failed_to_complete || err == computational_budget_exceeded); + + // Test num signers with 32-byte memo + let memo = Pubkey::new_unique().to_bytes(); + let mut keypairs = vec![]; + for _ in 0..20 { + keypairs.push(Keypair::new()); + } + let pubkeys: Vec = keypairs.iter().map(|keypair| keypair.pubkey()).collect(); + let signer_key_refs: Vec<&Pubkey> = pubkeys.iter().collect(); + + let mut signers = vec![&payer]; + for keypair in keypairs[..12].iter() { + signers.push(keypair); + } + let mut transaction = Transaction::new_with_payer( + &[build_memo(&memo, &signer_key_refs[..12])], + Some(&payer.pubkey()), + ); + transaction.sign(&signers, recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + + let mut signers = vec![&payer]; + for keypair in keypairs[..15].iter() { + signers.push(keypair); + } + let mut transaction = Transaction::new_with_payer( + &[build_memo(&memo, &signer_key_refs[..15])], + Some(&payer.pubkey()), + ); + transaction.sign(&signers, recent_blockhash); + let err = banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert!(err == failed_to_complete || err == computational_budget_exceeded); +}