Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 190e664

Browse files
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
1 parent 1c4753e commit 190e664

File tree

10 files changed

+439
-53
lines changed

10 files changed

+439
-53
lines changed

Cargo.lock

Lines changed: 8 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/src/memo.md

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
title: Memo Program
33
---
44

5-
A simple program that validates a string of UTF-8 encoded characters. It can be
6-
used to record a string on-chain, stored in the instruction data of a successful
7-
transaction.
5+
The Memo program is a simple program that validates a string of UTF-8 encoded
6+
characters and verifies that any accounts provided are signers of the
7+
transaction. The program also logs the memo, as well as any verified signer
8+
addresses, to the transaction log, so that anyone can easily observe memos and
9+
know they were approved by zero or more addresses by inspecting the transaction
10+
log from a trusted provider.
811

912
## Background
1013

@@ -22,9 +25,54 @@ The Memo Program's source is available on
2225
## Interface
2326

2427
The on-chain Memo Program is written in Rust and available on crates.io as
25-
[spl-memo](https://crates.io/crates/spl-memo).
28+
[spl-memo](https://crates.io/crates/spl-memo) and
29+
[docs.rs](https://docs.rs/spl-memo).
2630

27-
## Operational overview
31+
The crate provides a `build_memo()` method to easily create a properly
32+
constructed Instruction.
2833

29-
The Memo program attempts to UTF-8 decode the instruction data; if successfully
30-
decoded, the instruction is successful.
34+
## Operational Notes
35+
36+
If zero accounts are provided to the signed-memo instruction, the program
37+
succeeds when the memo is valid UTF-8, and logs the memo to the transaction log.
38+
39+
If one or more accounts are provided to the signed-memo instruction, all must be
40+
valid signers of the transaction for the instruction to succeed.
41+
42+
### Logs
43+
44+
This section details expected log output for memo instructions.
45+
46+
Logging begins with entry into the program:
47+
`Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr invoke [1]`
48+
49+
The program will include a separate log for each verified signer:
50+
`Program log: Signed by <BASE_58_ADDRESS>`
51+
52+
Then the program logs the memo length and UTF-8 text:
53+
`Program log: Memo (len 4): "🐆"`
54+
55+
If UTF-8 parsing fails, the program will log the failure point:
56+
`Program log: Invalid UTF-8, from byte 4`
57+
58+
Logging ends with the status of the instruction, one of:
59+
`Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr success`
60+
`Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr failed: missing required signature for instruction`
61+
`Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr failed: invalid instruction data`
62+
63+
For more information about exposing program logs on a node, head to the
64+
[developer
65+
docs](https://docs.solana.com/developing/deployed-programs/debugging#logging)
66+
67+
### Compute Limits
68+
69+
Like all programs, the Memo Program is subject to the cluster's [compute
70+
budget](https://docs.solana.com/developing/programming-model/runtime#compute-budget).
71+
In Memo, compute is used for parsing UTF-8, verifying signers, and logging,
72+
limiting the memo length and number of signers that can be processed
73+
successfully in a single instruction. The longer or more complex the UTF-8 memo,
74+
the fewer signers can be supported, and vice versa.
75+
76+
As of v1.5.1, an unsigned instruction can support single-byte UTF-8 of up to 566
77+
bytes. An instruction with a simple memo of 32 bytes can support up to 12
78+
signers.

memo/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# Memo Program
22

3-
A simple program that validates a string of UTF-8 encoded characters. It can be
4-
used to record a string on-chain, stored in the instruction data of a successful
5-
transaction.
3+
A simple program that validates a string of UTF-8 encoded characters and logs it
4+
in the transaction log. The program also verifies that any accounts provided are
5+
signers of the transaction, and if so, logs their addresses. It can be used to
6+
record a string on-chain, stored in the instruction data of a successful
7+
transaction, and optionally verify the originator.
68

79
Full documentation is available at https://spl.solana.com/memo

memo/program/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "spl-memo"
3-
version = "2.0.1"
3+
version = "3.0.0"
44
description = "Solana Program Library Memo"
55
authors = ["Solana Maintainers <[email protected]>"]
66
repository = "https://github.com/solana-labs/solana-program-library"
@@ -9,10 +9,16 @@ edition = "2018"
99

1010
[features]
1111
no-entrypoint = []
12+
test-bpf = []
1213

1314
[dependencies]
1415
solana-program = "1.5.1"
1516

17+
[dev-dependencies]
18+
solana-program-test = "1.5.1"
19+
solana-sdk = "1.5.1"
20+
tokio = { version = "0.3", features = ["macros"]}
21+
1622
[lib]
1723
crate-type = ["cdylib", "lib"]
1824

memo/program/program-id.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo
1+
MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr

memo/program/run-tests.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env bash
2+
3+
set -ex
4+
cd "$(dirname "$0")"
5+
cargo fmt -- --check
6+
cargo clippy
7+
cargo build
8+
cargo build-bpf
9+
10+
if [[ $1 = -v ]]; then
11+
export RUST_LOG=solana=debug
12+
fi
13+
14+
cargo test
15+
cargo test-bpf

memo/program/src/entrypoint.rs

Lines changed: 6 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,16 @@
11
//! Program entrypoint
22
3+
#![cfg(not(feature = "no-entrypoint"))]
4+
35
use solana_program::{
4-
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, program_error::ProgramError,
5-
pubkey::Pubkey,
6+
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey,
67
};
7-
use std::str::from_utf8;
88

99
entrypoint!(process_instruction);
1010
fn process_instruction(
11-
_program_id: &Pubkey,
12-
_accounts: &[AccountInfo],
11+
program_id: &Pubkey,
12+
accounts: &[AccountInfo],
1313
instruction_data: &[u8],
1414
) -> ProgramResult {
15-
from_utf8(instruction_data).map_err(|_| ProgramError::InvalidInstructionData)?;
16-
Ok(())
17-
}
18-
19-
#[cfg(test)]
20-
mod tests {
21-
use super::*;
22-
use solana_program::{program_error::ProgramError, pubkey::Pubkey};
23-
24-
#[test]
25-
fn test_utf8_memo() {
26-
let program_id = Pubkey::new(&[0; 32]);
27-
28-
let string = b"letters and such";
29-
assert_eq!(Ok(()), process_instruction(&program_id, &[], string));
30-
31-
let emoji = "🐆".as_bytes();
32-
let bytes = [0xF0, 0x9F, 0x90, 0x86];
33-
assert_eq!(emoji, bytes);
34-
assert_eq!(Ok(()), process_instruction(&program_id, &[], &emoji));
35-
36-
let mut bad_utf8 = bytes;
37-
bad_utf8[3] = 0xFF; // Invalid UTF-8 byte
38-
assert_eq!(
39-
Err(ProgramError::InvalidInstructionData),
40-
process_instruction(&program_id, &[], &bad_utf8)
41-
);
42-
}
15+
crate::processor::process_instruction(program_id, accounts, instruction_data)
4316
}

memo/program/src/lib.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,34 @@
11
#![deny(missing_docs)]
22

3-
//! A simple program that accepts a string of encoded characters and verifies that it parses. Currently handles UTF-8.
3+
//! A program that accepts a string of encoded characters and verifies that it parses,
4+
//! while verifying and logging signers. Currently handles UTF-8 characters.
45
5-
#[cfg(not(feature = "no-entrypoint"))]
66
mod entrypoint;
7+
pub mod processor;
78

89
// Export current sdk types for downstream users building with a different sdk version
910
pub use solana_program;
11+
use solana_program::{
12+
instruction::{AccountMeta, Instruction},
13+
pubkey::Pubkey,
14+
};
1015

11-
solana_program::declare_id!("Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo");
16+
solana_program::declare_id!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
17+
18+
/// Build a memo instruction, possibly signed
19+
///
20+
/// Accounts expected by this instruction:
21+
///
22+
/// 0. ..0+N. `[signer]` Expected signers; if zero provided, instruction will be processed as a
23+
/// normal, unsigned spl-memo
24+
///
25+
pub fn build_memo(memo: &[u8], signer_pubkeys: &[&Pubkey]) -> Instruction {
26+
Instruction {
27+
program_id: id(),
28+
accounts: signer_pubkeys
29+
.iter()
30+
.map(|&pubkey| AccountMeta::new_readonly(*pubkey, true))
31+
.collect(),
32+
data: memo.to_vec(),
33+
}
34+
}

memo/program/src/processor.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//! Program state processor
2+
3+
use solana_program::{
4+
account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError,
5+
pubkey::Pubkey,
6+
};
7+
use std::str::from_utf8;
8+
9+
/// Instruction processor
10+
pub fn process_instruction(
11+
_program_id: &Pubkey,
12+
accounts: &[AccountInfo],
13+
input: &[u8],
14+
) -> ProgramResult {
15+
let account_info_iter = &mut accounts.iter();
16+
let mut missing_required_signature = false;
17+
for account_info in account_info_iter {
18+
if let Some(address) = account_info.signer_key() {
19+
msg!("Signed by {:?}", address);
20+
} else {
21+
missing_required_signature = true;
22+
}
23+
}
24+
if missing_required_signature {
25+
return Err(ProgramError::MissingRequiredSignature);
26+
}
27+
28+
let memo = from_utf8(input).map_err(|err| {
29+
msg!("Invalid UTF-8, from byte {}", err.valid_up_to());
30+
ProgramError::InvalidInstructionData
31+
})?;
32+
msg!("Memo (len {}): {:?}", memo.len(), memo);
33+
34+
Ok(())
35+
}
36+
37+
#[cfg(test)]
38+
mod tests {
39+
use super::*;
40+
use solana_program::{
41+
account_info::IntoAccountInfo, program_error::ProgramError, pubkey::Pubkey,
42+
};
43+
use solana_sdk::account::Account;
44+
45+
#[test]
46+
fn test_utf8_memo() {
47+
let program_id = Pubkey::new(&[0; 32]);
48+
49+
let string = b"letters and such";
50+
assert_eq!(Ok(()), process_instruction(&program_id, &[], string));
51+
52+
let emoji = "🐆".as_bytes();
53+
let bytes = [0xF0, 0x9F, 0x90, 0x86];
54+
assert_eq!(emoji, bytes);
55+
assert_eq!(Ok(()), process_instruction(&program_id, &[], &emoji));
56+
57+
let mut bad_utf8 = bytes;
58+
bad_utf8[3] = 0xFF; // Invalid UTF-8 byte
59+
assert_eq!(
60+
Err(ProgramError::InvalidInstructionData),
61+
process_instruction(&program_id, &[], &bad_utf8)
62+
);
63+
}
64+
65+
#[test]
66+
fn test_signers() {
67+
let program_id = Pubkey::new(&[0; 32]);
68+
let memo = "🐆".as_bytes();
69+
70+
let pubkey0 = Pubkey::new_unique();
71+
let pubkey1 = Pubkey::new_unique();
72+
let pubkey2 = Pubkey::new_unique();
73+
let mut account0 = Account::default();
74+
let mut account1 = Account::default();
75+
let mut account2 = Account::default();
76+
77+
let signed_account_infos = vec![
78+
(&pubkey0, true, &mut account0).into_account_info(),
79+
(&pubkey1, true, &mut account1).into_account_info(),
80+
(&pubkey2, true, &mut account2).into_account_info(),
81+
];
82+
assert_eq!(
83+
Ok(()),
84+
process_instruction(&program_id, &signed_account_infos, memo)
85+
);
86+
87+
assert_eq!(Ok(()), process_instruction(&program_id, &[], memo));
88+
89+
let unsigned_account_infos = vec![
90+
(&pubkey0, false, &mut account0).into_account_info(),
91+
(&pubkey1, false, &mut account1).into_account_info(),
92+
(&pubkey2, false, &mut account2).into_account_info(),
93+
];
94+
assert_eq!(
95+
Err(ProgramError::MissingRequiredSignature),
96+
process_instruction(&program_id, &unsigned_account_infos, memo)
97+
);
98+
99+
let partially_signed_account_infos = vec![
100+
(&pubkey0, true, &mut account0).into_account_info(),
101+
(&pubkey1, false, &mut account1).into_account_info(),
102+
(&pubkey2, true, &mut account2).into_account_info(),
103+
];
104+
assert_eq!(
105+
Err(ProgramError::MissingRequiredSignature),
106+
process_instruction(&program_id, &partially_signed_account_infos, memo)
107+
);
108+
}
109+
}

0 commit comments

Comments
 (0)