Add initial signed-memo program (#1135)

* Initial s-memo

* Populate readme

* Add signed-memo to spl docs

* Log less, fail faster

* Replace and bump memo

* Update memo id

* Add memo prefix and len

* Add test that demonstrates compute bounds

* Add logging and compute to memo docs
This commit is contained in:
Tyera Eulberg 2021-01-28 12:21:21 -07:00 committed by GitHub
parent 1c4753e9a3
commit 190e664dd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 439 additions and 53 deletions

13
Cargo.lock generated
View File

@ -3554,7 +3554,7 @@ dependencies = [
"solana-sdk",
"solana-stake-program",
"solana-vote-program",
"spl-memo 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"spl-memo 2.0.1",
"spl-token 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"thiserror",
]
@ -3712,17 +3712,20 @@ dependencies = [
[[package]]
name = "spl-memo"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb2b771f6146dec14ef5fbf498f9374652c54badc3befc8c40c1d426dd45d720"
dependencies = [
"solana-program",
]
[[package]]
name = "spl-memo"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb2b771f6146dec14ef5fbf498f9374652c54badc3befc8c40c1d426dd45d720"
version = "3.0.0"
dependencies = [
"solana-program",
"solana-program-test",
"solana-sdk",
"tokio 0.3.6",
]
[[package]]
@ -4062,7 +4065,7 @@ name = "test-client"
version = "0.1.0"
dependencies = [
"solana-sdk",
"spl-memo 2.0.1",
"spl-memo 3.0.0",
"spl-token 3.0.1",
"spl-token-swap",
]

View File

@ -2,9 +2,12 @@
title: Memo Program
---
A simple program that validates a string of UTF-8 encoded characters. It can be
used to record a string on-chain, stored in the instruction data of a successful
transaction.
The Memo program is a simple program that validates a string of UTF-8 encoded
characters and verifies that any accounts provided are signers of the
transaction. The program also logs the memo, as well as any verified signer
addresses, to the transaction log, so that anyone can easily observe memos and
know they were approved by zero or more addresses by inspecting the transaction
log from a trusted provider.
## Background
@ -22,9 +25,54 @@ The Memo Program's source is available on
## Interface
The on-chain Memo Program is written in Rust and available on crates.io as
[spl-memo](https://crates.io/crates/spl-memo).
[spl-memo](https://crates.io/crates/spl-memo) and
[docs.rs](https://docs.rs/spl-memo).
## Operational overview
The crate provides a `build_memo()` method to easily create a properly
constructed Instruction.
The Memo program attempts to UTF-8 decode the instruction data; if successfully
decoded, the instruction is successful.
## Operational Notes
If zero accounts are provided to the signed-memo instruction, the program
succeeds when the memo is valid UTF-8, and logs the memo to the transaction log.
If one or more accounts are provided to the signed-memo instruction, all must be
valid signers of the transaction for the instruction to succeed.
### Logs
This section details expected log output for memo instructions.
Logging begins with entry into the program:
`Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr invoke [1]`
The program will include a separate log for each verified signer:
`Program log: Signed by <BASE_58_ADDRESS>`
Then the program logs the memo length and UTF-8 text:
`Program log: Memo (len 4): "🐆"`
If UTF-8 parsing fails, the program will log the failure point:
`Program log: Invalid UTF-8, from byte 4`
Logging ends with the status of the instruction, one of:
`Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr success`
`Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr failed: missing required signature for instruction`
`Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr failed: invalid instruction data`
For more information about exposing program logs on a node, head to the
[developer
docs](https://docs.solana.com/developing/deployed-programs/debugging#logging)
### Compute Limits
Like all programs, the Memo Program is subject to the cluster's [compute
budget](https://docs.solana.com/developing/programming-model/runtime#compute-budget).
In Memo, compute is used for parsing UTF-8, verifying signers, and logging,
limiting the memo length and number of signers that can be processed
successfully in a single instruction. The longer or more complex the UTF-8 memo,
the fewer signers can be supported, and vice versa.
As of v1.5.1, an unsigned instruction can support single-byte UTF-8 of up to 566
bytes. An instruction with a simple memo of 32 bytes can support up to 12
signers.

View File

@ -1,7 +1,9 @@
# Memo Program
A simple program that validates a string of UTF-8 encoded characters. It can be
used to record a string on-chain, stored in the instruction data of a successful
transaction.
A simple program that validates a string of UTF-8 encoded characters and logs it
in the transaction log. The program also verifies that any accounts provided are
signers of the transaction, and if so, logs their addresses. It can be used to
record a string on-chain, stored in the instruction data of a successful
transaction, and optionally verify the originator.
Full documentation is available at https://spl.solana.com/memo

View File

@ -1,6 +1,6 @@
[package]
name = "spl-memo"
version = "2.0.1"
version = "3.0.0"
description = "Solana Program Library Memo"
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
repository = "https://github.com/solana-labs/solana-program-library"
@ -9,10 +9,16 @@ edition = "2018"
[features]
no-entrypoint = []
test-bpf = []
[dependencies]
solana-program = "1.5.1"
[dev-dependencies]
solana-program-test = "1.5.1"
solana-sdk = "1.5.1"
tokio = { version = "0.3", features = ["macros"]}
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -1 +1 @@
Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo
MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr

15
memo/program/run-tests.sh Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -ex
cd "$(dirname "$0")"
cargo fmt -- --check
cargo clippy
cargo build
cargo build-bpf
if [[ $1 = -v ]]; then
export RUST_LOG=solana=debug
fi
cargo test
cargo test-bpf

View File

@ -1,43 +1,16 @@
//! Program entrypoint
#![cfg(not(feature = "no-entrypoint"))]
use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, program_error::ProgramError,
pubkey::Pubkey,
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey,
};
use std::str::from_utf8;
entrypoint!(process_instruction);
fn process_instruction(
_program_id: &Pubkey,
_accounts: &[AccountInfo],
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
from_utf8(instruction_data).map_err(|_| ProgramError::InvalidInstructionData)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use solana_program::{program_error::ProgramError, pubkey::Pubkey};
#[test]
fn test_utf8_memo() {
let program_id = Pubkey::new(&[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)
);
}
crate::processor::process_instruction(program_id, accounts, instruction_data)
}

View File

@ -1,11 +1,34 @@
#![deny(missing_docs)]
//! A simple program that accepts a string of encoded characters and verifies that it parses. Currently handles UTF-8.
//! A program that accepts a string of encoded characters and verifies that it parses,
//! while verifying and logging signers. Currently handles UTF-8 characters.
#[cfg(not(feature = "no-entrypoint"))]
mod entrypoint;
pub mod processor;
// Export current sdk types for downstream users building with a different sdk version
pub use solana_program;
use solana_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
solana_program::declare_id!("Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo");
solana_program::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(),
}
}

View File

@ -0,0 +1,109 @@
//! Program state processor
use solana_program::{
account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError,
pubkey::Pubkey,
};
use 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::*;
use solana_program::{
account_info::IntoAccountInfo, program_error::ProgramError, pubkey::Pubkey,
};
use solana_sdk::account::Account;
#[test]
fn test_utf8_memo() {
let program_id = Pubkey::new(&[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(&[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)
);
}
}

View File

@ -0,0 +1,207 @@
#![cfg(feature = "test-bpf")]
use solana_program::{
instruction::{AccountMeta, Instruction, InstructionError},
pubkey::Pubkey,
};
use solana_program_test::*;
use solana_sdk::{
signature::{Keypair, Signer},
transaction::{Transaction, TransactionError},
};
use 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 (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let keypairs = vec![Keypair::new(), Keypair::new(), Keypair::new()];
let pubkeys: Vec<Pubkey> = 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]
async fn test_memo_compute_limits() {
let (mut 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[..566], &[])], 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[..567], &[])], Some(&payer.pubkey()));
transaction.sign(&[&payer], recent_blockhash);
assert_eq!(
banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap(),
TransactionError::InstructionError(0, InstructionError::ProgramFailedToComplete)
);
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);
assert_eq!(
banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap(),
TransactionError::InstructionError(0, InstructionError::ProgramFailedToComplete)
);
// 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<Pubkey> = 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[..13].iter() {
signers.push(keypair);
}
let mut transaction = Transaction::new_with_payer(
&[build_memo(&memo, &signer_key_refs[..13])],
Some(&payer.pubkey()),
);
transaction.sign(&signers, recent_blockhash);
assert_eq!(
banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap(),
TransactionError::InstructionError(0, InstructionError::ProgramFailedToComplete)
);
}