Skip to content

Instantly share code, notes, and snippets.

@keitaj
Created April 23, 2022 03:20
Show Gist options
  • Save keitaj/c6bce4dbd7f4f23bbadfc339e45691ca to your computer and use it in GitHub Desktop.
Save keitaj/c6bce4dbd7f4f23bbadfc339e45691ca to your computer and use it in GitHub Desktop.
Solana Program Tutorial
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
/// Define the type of state stored in accounts
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct GreetingAccount {
/// number of greetings
pub counter: u32,
}
// Declare and export the program's entrypoint
entrypoint!(process_instruction);
// Program entrypoint's implementation
pub fn process_instruction(
program_id: &Pubkey, // Public key of the account the hello world program was loaded into
accounts: &[AccountInfo], // The account to say hello to
_instruction_data: &[u8], // Ignored, all helloworld instructions are hellos
) -> ProgramResult {
msg!("Hello World Rust program entrypoint");
// Iterating accounts is safer than indexing
let accounts_iter = &mut accounts.iter();
// Get the account to say hello to
let account = next_account_info(accounts_iter)?;
// The account must be owned by the program in order to modify its data
if account.owner != program_id {
msg!("Greeted account does not have the correct program id");
return Err(ProgramError::IncorrectProgramId);
}
// Increment and store the number of times the account has been greeted
let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;
greeting_account.counter += 1;
greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?;
msg!("Greeted {} time(s)!", greeting_account.counter);
Ok(())
}
// Sanity tests
#[cfg(test)]
mod test {
use super::*;
use solana_program::clock::Epoch;
use std::mem;
#[test]
fn test_sanity() {
let program_id = Pubkey::default();
let key = Pubkey::default();
let mut lamports = 0;
let mut data = vec![0; mem::size_of::<u32>()];
let owner = Pubkey::default();
let account = AccountInfo::new(
&key,
false,
true,
&mut lamports,
&mut data,
&owner,
false,
Epoch::default(),
);
let instruction_data: Vec<u8> = Vec::new();
let accounts = vec![account];
assert_eq!(
GreetingAccount::try_from_slice(&accounts[0].data.borrow())
.unwrap()
.counter,
0
);
process_instruction(&program_id, &accounts, &instruction_data).unwrap();
assert_eq!(
GreetingAccount::try_from_slice(&accounts[0].data.borrow())
.unwrap()
.counter,
1
);
process_instruction(&program_id, &accounts, &instruction_data).unwrap();
assert_eq!(
GreetingAccount::try_from_slice(&accounts[0].data.borrow())
.unwrap()
.counter,
2
);
}
}
@keitaj
Copy link
Author

keitaj commented Apr 23, 2022

// contracts/solana/program/src/lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    program_error::ProgramError,
    pubkey::Pubkey,
};

Rustでは、use宣言は他のコードへの便利なショートカットです。この場合、borshクレートのシリアライズ関数とデシリアライズ関数です。borshはBinary Object Representation Serializer for Hashingの略です。

Solana Programのクレートの一部を使用する。クレートとは、配布やコンパイルが可能なソースコードの集まりのこと(バイナリやライブラリ)です。

  • AccountInfo の構造体と同様に、次の AccountInfo を返す関数
  • entrypointと関連するentrypoint::ProgramResult
  • ブロックチェーンに負荷をかけないロギングを行うためのmsg
  • program_error::ProgramError は、オンチェーンプログラムがプログラム固有のエラータイプを実装し、Solana ランタイムが返すエラータイプを見ることができるようにします。プログラム固有のエラーは、u32 整数として表現されるもの、または u32 整数に直列化されるものであれば、どのような型でもかまいません。
  • pubkey::Pubkeyの構造体

@keitaj
Copy link
Author

keitaj commented Apr 23, 2022

/// Define the type of state stored in accounts
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct GreetingAccount {
    /// number of greetings
    pub counter: u32,
}

次に、deriveマクロを使用してGreetingAccount構造体をラップするために必要な定型コードをすべて生成します。これはコンパイル時に#[derive()]マクロを使用することで裏側で行われます。Rustのマクロはかなり大きなトピックですが、理解するために努力する価値は十分にあります。とりあえず、これはコンパイル時に挿入される定型的なコードのショートカットであることだけは知っておいてください。

構造体宣言自体は簡単で、pubキーワードを使用して構造体をパブリックにアクセス可能であることを宣言しています。この構造体キーワードは、GreetingAccountという構造体を定義していることをコンパイラに知らせています。この構造体は、u32(符号なし32ビット整数)の型を持つcounterという単一のフィールドを持っています。つまり、カウンタは4,294,967,295より大きくてはいけないということです。

@keitaj
Copy link
Author

keitaj commented Apr 23, 2022

次に、エントリポイントであるprocess_instruction関数を宣言します。

entrypoint!(process_instruction);

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    msg!("Hello World Rust program entrypoint");
    let accounts_iter = &mut accounts.iter();
    let account = next_account_info(accounts_iter)?;
  • process_instruction エントリポイントの戻り値は ProgramResult になります。
    Result は std クレートに由来し、エラーの可能性を表現するために使用されます。
  • デバッグのために、ネットワークの計算コストが高くつくprintln!()を使わず、msg!()マクロでProgram Logにメッセージを出力することができます。
  • Rustのletキーワードは変数の値を不変にします。イテレータを使ってアカウントをループすることで、accounts_iterはaccountsの各値のミュータブルリファレンスを取っています。そしてnext_account_info(account_iter)?は次のAccountInfoを返すかNotEnoughAccountKeysエラーを返します。最後の?に注目してください。これはRustのエラー伝播のためのショートカット式です。
// The account must be owned by the program in order to modify its data
if account.owner != program_id {
  msg!("Greeted account does not have the correct program id");
  return Err(ProgramError::IncorrectProgramId);
}

アカウントの所有者が許可されているかどうか、セキュリティチェックを行います。もし account.owner の公開鍵が program_id と等しくない場合は、エラーを返します。

let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;
greeting_account.counter += 1;
greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?;

msg!("Greeted {} time(s)!", greeting_account.counter);

Ok(())

最後に、既存のアカウントデータを「借りて」、カウンターの値を1つ増やし、ストレージに書き戻すという、いいとこ取りの処理をします。

  • GreetingAccount構造体はcounterという1つのフィールドしか持っていません。これを変更できるようにするには、&borrow演算子でaccount.dataの参照を借用する必要があります。
  • BorshDeserialize の try_from_slice() 関数は、 account.data を mutable で参照し、デシリアライズします。
  • borrow()関数はRustコアライブラリに由来し、ラップされた値を不変に借用するために存在します。

つまり、アカウントデータを借りてきて、それをデシリアライズする関数に渡し、エラーが発生した場合はエラーを返すということです。?はエラー伝搬のためのものであることを思い出してください。

次に、カウンターの値を1つ増やすのは、加算代入演算子+=を使えば簡単です。

BorshSerializeのserialize()関数により、新しいカウンタ値が正しいフォーマットでSolanaに送り返されます。このメカニズムは、std::ioクレートのWrite traitによって実現されています。

そして、そのカウントが何回インクリメントされたかは、msg!を使って見ることができる。

@keitaj
Copy link
Author

keitaj commented Apr 23, 2022

Set up Solana CLI

Solana Programをデプロイする前に、Solanaクラスターを設定し、アカウントを作成し、エアドロップを要求し、すべてが正しく機能していることを確認する必要があります。
CLI の設定 URL を devnet クラスターに設定します。

solana config set --url https://api.devnet.solana.com

次に、CLIを使用して新しいキーペアを生成します。ターミナルで以下のコマンドを実行します。

mkdir solana-wallet
solana-keygen new --outfile solana-wallet/keypair.json

デプロイには、アカウントで利用できるSOLが必要ですので、AirDrop withを入手してください。

solana airdrop 1 $(solana-keygen pubkey solana-wallet/keypair.json)

すべての設定が完了し、アドレスに1SOLの資金が投入されたことを確認します。

solana config get
solana account $(solana-keygen pubkey solana-wallet/keypair.json)

@keitaj
Copy link
Author

keitaj commented Apr 23, 2022

Solana programデプロイする前にRustのインストールが必要
https://doc.rust-jp.rs/book-ja/ch01-01-installation.html

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

Filesize limit exceededのエラー出たらとりあえずこれで対応

ulimit -f unlimited

インストールしたらPATH通す

export PATH="$HOME/.cargo/bin:$PATH"

rustを最新版にアップデート

rustup update

rustcのバージョン確認

rustc --version

@keitaj
Copy link
Author

keitaj commented Apr 23, 2022

Deploy a Solana program

これからデプロイするプログラムは、あるアカウントが挨拶の指示を送った回数を記録しています。これは、Solana上でストレージがどのように機能するかを効果的に示すものです。

Building the program

yarn run solana:build:program

成功すると、helloworld.soというコンパイル済みのプログラムへのパスを指定してdeployコマンドを実行するように指示されます。このままでもいいのですが、この目的のためだけに生成したキーペアを指定したいので、読み進めてください。

To deploy this program:
  $ solana program deploy /home/zu/project/figment/learn-web3-dapp/dist/solana/program/helloworld.so
Done in 1.39s.

Deploying the program

CLIのsolana deployを使って、プログラムをdevnetクラスタにデプロイする

solana deploy -v --keypair solana-wallet/keypair.json dist/solana/program/helloworld.so

vフラグはオプションですが、RPC URLやデフォルトの署名者鍵ペアのパス、予想されるコミットメントレベルなどの関連情報が表示されます。プロセスが完了すると、プログラムIDが表示されます。
成功した場合、CLIはデプロイされたコントラクトのprogramIdを表示します。

RPC URL: https://api.devnet.solana.com
Default Signer Path: solana-wallet/keypair.json
Commitment: confirmed
Program Id: 8LwWMLn37RKFLFz84HwaigLfpeoXCaXSFhcXooeDQBpW

Deploying the program to a test validator inside Gitpod

クラスタの変更

solana config set --url http://127.0.0.1:8899

テストバリデーターの実行

solana-test-validator

Gitpodで新しいターミナルを開きます (あるいはテストバリデータを実行しているターミナルを分割します)。この新しいターミナルで、Solana CLI の場所をコマンドで PATH に追加する必要があります。

export PATH="/home/gitpod/.local/share/solana/install/active_release/bin:$PATH"

/solana-wallet/keypair.json にあるキーペアに SOL の残高があることを確認し、SOL をいくらかエアドロップすることでデプロイの代金を支払います (テストバリデータ上なので、もっと高額な SOL を指定することも可能です)。

solana airdrop 100 $(solana-keygen pubkey solana-wallet/keypair.json)

これで、プログラムをテストバリデータにデプロイするコマンドを実行することができます。

solana deploy -v --keypair solana-wallet/keypair.json dist/solana/program/helloworld.so

@keitaj
Copy link
Author

keitaj commented Apr 23, 2022

Refs

% solana --version
solana-cli 1.10.5 (src:5eb085fc; feat:3235626988)
% rustc --version
rustc 1.60.0 (7737e0b5c 2022-04-04)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment