Hey glass chewers.

If you are an Anchor developer on Solana, you probably want to know how to optimize your program to reduce compute unit consumption. The well respected Andy posted a challenge on Twitter:

https://x.com/HeyAndyS/status/1803448339528061249

There are two instructions in this program and one data account. The first Instruction, store_memo(), consumes 33,531 CUs and the second instruction, validate_memo(), consumes 32,872 CUs:

pub struct MemoAccount {
    memo: String,
    authority: String,
    state: u64,
}
pub fn store_memo(
	ctx: Context<MemoContext>, 
	_memo_index: u32, 
	memo: String
) -> Result<()> {
        msg!("Program started!");

        let mut memo_len = 0u64;
        for _ch in memo.as_bytes() {
            memo_len += 1;
        }
        if memo_len > MAX_MEMO_SIZE {
            return err!(MyError::MemoTooLong);
        }

        msg!("Memo created by {}: {}", ctx.accounts.signer.key(), memo);

        ctx.accounts.new_account.memo = memo;
        ctx.accounts.new_account.state = INITIALIZED;
        ctx.accounts.new_account.authority = ctx.accounts.signer.key().to_string();

        msg!("Program successul!");
        Ok(())
 }
pub fn validate_memo(
	ctx: Context<ValidateContext>, 
	_memo_index: u32
) -> Result<()> {
        msg!("Program started!");

        if ctx.accounts.memo_account.state == UNINITIALIZED {
            msg!("This memo has not been created yet.");
            return err!(MyError::MemoUnititialized);
        } else if ctx.accounts.memo_account.state == VALID {
            msg!("This memo has already been validated.");
            return err!(MyError::MemoValidated);
        } else if ctx.accounts.memo_account.state == INVALIDATED {
            msg!("This memo has already been invalidated.");
            return err!(MyError::MemoInValidated);
        }
        msg!("Let's validated this memo");

        msg!("Validating the owner...");
        if ctx
            .accounts
            .memo_account
            .authority
            .eq(&ctx.accounts.signer.key().to_string())
        {
            msg!("owner valid.");
        } else {
            return err!(MyError::MemoOwnerDoesntMatch);
        }

        msg!(
            "is the memo valid? {}",
            is_valid_memo(ctx.accounts.memo_account.memo.clone())
        );

        if !is_valid_memo(ctx.accounts.memo_account.memo.clone()) {
            return err!(MyError::MemoIsNotValid);
        }
        if is_valid_memo(ctx.accounts.memo_account.memo.clone()) {
            ctx.accounts.memo_account.state = VALID;
        }

        msg!("Program successul!");
        Ok(())
}

Things that stand out are:

Let’s walkthrough some optimizations starting with MemoAccount :

pub struct MemoAccount {
    memo: [u8; 512],
    authority: Pubkey,
    state: u8,
}

We have:

In the client (i.e. tests) we can serialize a string to the required type using a small helper function:

/**
 * Encode a name into a fixed size array of u8s
 * @param name string to encode
 * @param size number of u8s to encode into
 * @returns array of u8s
 */
function encodeName(name: string, size: number = 512): number[] {
  // validate name length
  if (name.length > size) {
    throw Error(`Name (${name}) longer than ${size} characters`);
  }
  const buffer = Buffer.alloc(size)   // allocate a buffer of the correct size
    .fill(name)                       // fill the buffer with the name
    .fill(" ", name.length);          // pad the buffer with spaces to align with the size
  return Array(...buffer);
}