🍣

Solana, Hello world スマートコントラクトの説明

2022/07/25に公開

Solanaの開発者向けの非常にわかりやすい学習教材があったため紹介します。
https://www.notion.so/metacamp-community/Solana-Developer-Playbook-a4d62a4cd1244ed7abe44f340f0cd8a7

こちらのスマートコントラクトチュートリアルの第一回目のサンプルを紹介していきたいと思います。
https://docs.google.com/document/d/e/2PACX-1vSOgwdz9-vpBDwh3Epr3fdjzGyMWB1GHNT4H7YysNRyBFRJ0_qpcafgGcZUgNJLoyTH9IBVBaaInHsc/pub

Githubにソースコードも上がっています。
https://github.com/pappas999/gm-program

Solanaの専門用語、Accountについて

Solanaで開発を始めるにあたって、まずaccountという概念を理解する必要があります。
https://docs.solana.com/developing/programming-model/accounts

Accountとは、Solanaの専門用語で、Solana上でデータを保存するためのファイルのようなものです。Solanaでは、あらゆる情報がaccountに保存されています。ひとつひとつのaccountには、固有のアドレス(公開鍵)が割り振られています。

たとえばスマートコントラクトで、Aliceが保有しているトークンの残高を管理したい場合は、Aliceのトークン残高を保存するためのalice token accountを作成し、そのaccountに対しデータの読み書きを行います。Bobの残高の管理には、Bob専用のbob token accountを使用します。このようにSolanaでは、スマートコントラクトに加えて、データ保存用の各種accountも用意する必要があります。

Solanaでは、いわゆるスマートコントラクトのことをprogramと呼びます。実はこのprogramもaccountです。Solanaでprogramをdeployすると、programのロジックがprogram accountに保存されます。program accountはロジックデータのみを持ち、ステートの管理には、別途データ保存用のaccountが必要になります。このようにSolanaでは、スマートコントラクトのロジックとステートを完全に切り離して管理します。

サンプルプログラムの説明

src/lib.rsがprogramになります。上の方でGreetingAccountというstructが定義されていますが、これがprogramが管理するステートに相当します。process_instructionで、データを保存するためのaccountを指定し、保存するデータをinput: &[u8]で入力します。inputはbytesで入力されるので、try_from_sliceでdeserializeしてstructに復元した後、serializeで再びbytesにして、accountに保存するという処理を行っています。

...
/// Define the type of state stored in accounts
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct GreetingAccount {
    pub name: String,
}

// 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 GM program was loaded into
    accounts: &[AccountInfo], // The account to say GM to
    input: &[u8], // String input data, contains the name to say GM to
) -> ProgramResult {
    msg!("GM program entrypoint");

    // Iterating accounts is safer than indexing
    let accounts_iter = &mut accounts.iter();

    // Get the account to say GM 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);
    }

    // Deserialize the input data, and store it in a GreetingAccout struct
    let input_data = GreetingAccount::try_from_slice(&input).unwrap();

    //Say GM in the Program output
    msg!("GM {}", input_data.name);

    //Serialize the name, and store it in the passed in account
    input_data.serialize(&mut &mut account.try_borrow_mut_data()?[..])?;

    Ok(())
}

Clientからtransactionを実行してみる

programだけ見てもなにをやっているか分かりづらいですが、clientからtxを実行してみると何をやっているかが見えてくると思います。src/client/main.tsにtx実行用のスクリプトが書いてあります。ローカルノードを立ち上げて、programをdeployしたあとmain.tsを実行すると上記のprocess_instructionをローカルでテストできます。clientでの処理は以下のような流れになります。

  1. establishConnectionでローカルノードに接続する
  2. establishPayerでウォレットを接続する
  3. checkProgramでprogramがdeployされているかチェックし、データ保存用のaccountを作成する
  4. sayGmでprocess_instructionを実行する
  5. reportGmでaccountのデータを読み取り、実際に値が更新されているか確認する

src/client/gm_program.tsに具体的な処理が書いてあります。clientの処理には、@solana/web3.jsというSolana専用のjsモジュールを使用します。

accountの作成スクリプト

accountの概念を理解するため、account作成のためのスクリプトを見てみます。はじめにaccountのアドレスをcreateWithSeedから生成します。payerにはaccountを作成する人のアドレス、NAME_FOR_GMに今回保存するGreetingAccount.nameの文字列、programIdにdeployしたprogramのアドレスを入力します。payerを引数にすることでユーザー固有のaccountアドレスが生成できます。

    // Derive the address (public key) of a greeting account from the program so that it's easy to find later.
    greetedPubkey = await PublicKey.createWithSeed(
        payer.publicKey,
        NAME_FOR_GM,
        programId,
    );

次にcreateAccountWithSeedでaccountを作成します。この操作でgreetedPubkeyをアドレスとして持つ新規accountが作成されます。spaceという引数がありますが、これはaccountのデータサイズが何バイトかを指定するパラメータです。Solanaでは、accountのデータサイズに応じて、accountを存続させるための手数料をSOLで支払う必要があります。事前にgetMinimumBalanceForRentExemptionでaccount存続のための最低rentを計算し、lamportsフィールドに入力してrentの支払いを行っています。もしrentが足りない場合エラーとなります。最後のprogramIdフィールドがacccountのオーナーとなるため、ここにdeployしたprogramアドレスを入力することで、programからaccount情報を書き換えられるようになります。

        const lamports = await connection.getMinimumBalanceForRentExemption(
            GREETING_SIZE,
        );

        const transaction = new Transaction().add(
            SystemProgram.createAccountWithSeed({
                fromPubkey: payer.publicKey,
                basePubkey: payer.publicKey,
                seed: NAME_FOR_GM,
                newAccountPubkey: greetedPubkey,
                lamports,
                space: GREETING_SIZE,
                programId,
            }),
        );
        await sendAndConfirmTransaction(connection, transaction, [payer]);

process_instructionの実行

inputとなるGreetingAccountオブジェクトをserializeしてbytesにして、instructionを送ります。
keysがprocess_instructionの引数であるaccountsに対応し、dataがinputに対応しています。

    let data = borsh.serialize(GmAccount.schema, gm);
    const data_to_send = Buffer.from(data);
    console.log(data_to_send)

    const instruction = new TransactionInstruction({
        keys: [{ pubkey: greetedPubkey, isSigner: false, isWritable: true }],
        programId,
        data: data_to_send
    });
    await sendAndConfirmTransaction(
        connection,
        new Transaction().add(instruction),
        [payer],
    );

accountの情報の確認

getAccountInfoメソッドで、accountに保存されているデータを取得できます。得られる情報はbytesのため、deserializeすることで実際の値を確認できます。

export async function reportGm(): Promise<void> {
    const accountInfo = await connection.getAccountInfo(greetedPubkey);
    if (accountInfo === null) {
        throw 'Error: cannot find the greeted account';
    }
    const greeting = borsh.deserialize(
        GmAccount.schema,
        GmAccount,
        accountInfo.data,
    );
    console.log(
        greetedPubkey.toBase58(),
        'GM was said to ',
        greeting.name
    );
}

実行結果

main.tsを実行すると以下のような結果が得られます。Glass Chewerという文字列をinputとしてtxを実行し、作成したaccountにGlass Chewerが保存されていることが確認できます。

Using program 7DShxMcwxg3kZPkFCoGEAvwu7XzZpXWpfhXYJ3R39HBT
Creating account AfaVBfqqXo63g8rLkAFfaomFb21mWm4dRBbcdvTp76Ht to say hello to
Saying hello to  Glass Chewer  with key  AfaVBfqqXo63g8rLkAFfaomFb21mWm4dRBbcdvTp76Ht
<Buffer 0c 00 00 00 47 6c 61 73 73 20 43 68 65 77 65 72>
AfaVBfqqXo63g8rLkAFfaomFb21mWm4dRBbcdvTp76Ht GM was said to  Glass Chewer
Success

まとめ

Solanaではaccountを使ってデータの管理をしている。実際のデータのやり取りはbytesで行うため、適宜serialize, deserializeが必要となる。

Discussion