12
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

朝日新聞社Advent Calendar 2024

Day 12

SolanaでSBT(Soulbound Token)を発行してみた

Last updated at Posted at 2024-12-11

はじめに

 この記事は朝日新聞社 Advent Calendar 2024の12日目の記事です。
 はじめまして。メディア研究開発センターに所属している松村 圭貴(まつむら よしき)と申します。この冬もかなり寒くなってきましたね。私はこの冬、ボーナスが入ったら私用のPCを買い替えたいな〜と考えてます。
 今回は、仕事とは関係なく、私がプライベートで東京大学のブロックチェーンに関する公開講座を受講しており、せっかくなので公開講座で学んだ知見を活かしてブロックチェーンを利用した技術について紹介・実装していきたいと思います。

 公開講座の方では主にEthereumやビットコインプロトコルなどをメインに取り扱っていましたが、この記事では、2020年3月にローンチされたSolanaと呼ばれるブロックチェーンを取り扱います。

目次

  1. ブロックチェーンとは
  2. Solanaとは
  3. SBTとは
  4. 開発環境のセットアップ
  5. コントラクト(プログラム)の実装
  6. テストの作成
  7. デプロイと動作確認
  8. まとめ

1. ブロックチェーンとは

 まずブロックチェーンについて簡単に説明します。ブロックチェーンとは、「複数のコンピュータで取引記録を共有・検証し、改ざんが極めて困難な分散型システム」です。複数のコンピュータで取引データを検証し、全てのコンピュータで検証結果が一致したものをだけを台帳に記録するイメージです。
 ブロックチェーンにデータが載るとそのデータは一生書き換えることができません。この技術の応用例として代表的なものとして挙げられるのがビットコインなどの仮想通貨です。

2. Solanaとは

 Solanaは、ビットコインと同じ仮想通貨の一種であり、2020年3月にローンチされた暗号資産です。Solanaとは、ビットコインプロトコルとは異なり、スマートコントラクトを採用している暗号資産になります。
 スマートコントラクトとはブロックチェーンに保存されたプログラムであり、所定の条件が満たされた場合に実行されます。ビットコインプロトコルは基本的な金銭取引データのみを扱いますが、Solanaではプログラム自体がブロックチェーンに記録されます。これによって複雑なビジネスロジックを実現することが可能になります。

3. SBTとは

概要

  • NFT
     SBTを説明する前にNFTについて少し解説をします。NFTとはNon Fungible Tokenの略で、非代替性トークンと呼ばれます。NFTは代替可能性がない唯一無二のデータであり、ビットコインなどの暗号通貨は反対にFungible Tokenと呼ばれます。このNFTを利用してアートなどの作品に対してNFTを紐づけることで、絵画や音楽などの所有権の売買に利用され、投資の場面で利用されるケースが多いです。
    image.png

  • SBT
     SBTとはSoulbound Tokenの略になります。Soulboundとは「魂に紐づいた」などの意味がありますが、このSBTは所有権を譲渡できません。つまり、一度発行されたSBTの所有者は永遠に変わることはありません。このSBTは、例えば職歴、学歴、資格などの不変の記録に使用することができます。言い方を変えると、譲渡することに意味はないがその人が何かを証明する際に役立つトークンとして利用されるケースが多いです。
    image.png

技術的特徴

マルチ言語対応
Solanaは、主に使われている言語はRustですが、他にもTypescriptやGoなどでも開発をすることができます。
転送不可能な仕組み
SBTはNFTの一種であり、NFTを譲渡不可能にする処理を追加することで実現できます。そのため基本的な実装はNFTを発行する手順と同じになります。

それでは次の項目から実装していきましょう!

4. 開発環境のセットアップ

実装開始の前に、まずは環境構築を行っていきましょう。

Rust のインストール

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

Solana CLI のインストール

sh -c "$(curl -sSfL https://release.solana.com/v1.17.0/install)"

Anchor のインストール

Anchor:Solanaプログラムを安全かつ迅速に構築するためのフレームワークになります。

cargo install --git https://github.com/coral-xyz/anchor avm --locked --force

最新でない場合は以下のコマンドを実行

avm install latest
avm use latest

5. プログラムの実装

いよいよここから実装開始です。

プロジェクトの作成

anchor init mint_sbt
cd mint_sbt

Anchor.tomlファイルの設定

tomlファイルは以下のものを貼り付けてください。solanaのバージョンは個人の環境に合わせて変えてください。solana -Vでバージョン確認ができます。

[toolchain]
anchor_version = "0.30.1"
solana_version = "1.18.26"

[features]
resolution = true
skip-lint = false

[programs.localnet]
mint_sbt = "11111111111111111111111111111111"

[programs.devnet]
mint_sbt = "11111111111111111111111111111111"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "devnet"
wallet = "~/.config/solana/id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

[workspace]
types = "docker"
members = ["programs/*"]

solana用のwalletが存在しない場合はwalletを作成

*公開鍵と秘密鍵は控えておく

solana-keygen new
  • アドレス(公開鍵)確認コマンド
solana address -k ~/.config/solana/id.json
  • devnet残高の確認
solana balance --url devnet
  • 残高がなければdevnet用のsolanaをもらう
solana airdrop 10 --url devnet
  • devnet(検証用のブロックチェーン環境)にデプロイするため、Solanaの設定をdevnetに変更・確認を行う
solana config set --url devnet
solana config get

SBTプログラムの実装

  • 本来はMetaplexが提供するプロトコルを通じてNFT・SBTを発行するが、今回は学習も兼ねているため独自にSBT発行プログラムを実装し、発行していきます。
  • programs/mint-sbt/src/lib.rsに以下のコードを記述。
use anchor_lang::prelude::*;

declare_id!("11111111111111111111111111111111");

#[program]
pub mod my_sbt_project {
    use super::*;
    // 初期化
    pub fn initialize(ctx: Context<Initialize>, data: String) -> Result<()> {
        let sbt_account = &mut ctx.accounts.sbt_account;
        sbt_account.data = data;
        sbt_account.owner = ctx.accounts.user.key();
        sbt_account.is_transferable = false;
        Ok(())
    }
    // 転送
    pub fn transfer(ctx: Context<Transfer>) -> Result<()> {
        let sbt_account = &ctx.accounts.sbt_account;
        
        // 転送可能かどうかをチェック
        require!(sbt_account.is_transferable, ErrorCode::NonTransferable);

        // 所有者チェック
        require!(
            ctx.accounts.current_owner.key() == sbt_account.owner,
            ErrorCode::InvalidOwner
        );

        // 転送処理
        let sbt_account = &mut ctx.accounts.sbt_account;
        sbt_account.owner = ctx.accounts.new_owner.key();

        Ok(())
    }
}
#[derive(Accounts)]
pub struct Initialize<'info> {
    // 初期化するSBTアカウント
    #[account(init, payer = user, space = 8 + 64)]
    pub sbt_account: Account<'info, SBTAccount>,
    // 初期化するユーザー
    #[account(mut)]
    pub user: Signer<'info>,
    // システムプログラム
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Transfer<'info> {
    #[account(mut)]
    pub sbt_account: Account<'info, SBTAccount>,
    pub current_owner: Signer<'info>,
    /// CHECK: new_ownerは単なるアドレスの受信者であり、プログラムの安全性に影響を与えません
    pub new_owner: AccountInfo<'info>,
}

#[account]
pub struct SBTAccount {
    // SBTアカウントに格納されたデータ
    pub data: String,
    // SBTアカウントの所有者
    pub owner: Pubkey,
    // SBTアカウントが転送可能かどうか
    pub is_transferable: bool,
}

#[error_code]
pub enum ErrorCode {
    #[msg("This SBT is non-transferable")]
    NonTransferable,
    #[msg("Only the owner can transfer this SBT")]
    InvalidOwner,
}
/// CHECK: new_ownerは安全であることを確認済みです。なぜなら...
pub struct NewOwner {
    pub new_owner: Pubkey,
}

この実装でのポイントは、まず初期化処理でsbt_account.is_transferable = false;になっていることです。技術的特徴でも述べたようにSBTはNFTを転送不可能にしたものになります。そのため最初から初期化の段階で転送できないようにすることでSBTにすることが可能になります。それからtransfer関数内で転送可能かとして以下のチェックを入れています。

require!(sbt_account.is_transferable, ErrorCode::NonTransferable);

これにより転送命令が出たとしてもエラーとして転送不可能になります。

ビルド

anchor build
  • ビルド後にプログラムIDが生成されるため確認
solana address -k target/deploy/mint_sbt-keypair.json

8eUF79XJRsRAKaCgTiPpvREgPwkshCbPj5x9b7cjZFsG
  • プログラムIDが生成されたので、仮のプログラムIDを入れていた箇所を上記のプログラムIDに差し替える。

devnetへデプロイ

mint-sbt % anchor deploy

Deploying cluster: https://api.devnet.solana.com
Upgrade authority: /Users/User/.config/solana/id.json
Deploying program "mint_sbt"...
Program path: /Users/User/.../sbt/mint-sbt/target/deploy/mint_sbt.so...
Program Id: 8eUF79XJRsRAKaCgTiPpvREgPwkshCbPj5x9b7cjZFsG

Deploy success

solana explorerで確認することができる。

SBT発行スクリプト(Typescript)

AnchorのフレームワークではTypescriptとRust言語の両方を使ってsolanaのブロックチェーン開発を行う

言語 使い分け
Rust ブロックチェーン上で実行される全ての処理(例:アカウントの作成、データの更新、トークンの転送など)
Typescript ブロックチェーンと対話するクライアント側の処理やテスト(例:トランザクションの送信、アカウントデータの取得、ユーザー操作の処理)
  • モジュールインストール
yarn add dotenv
  • .envファイルの作成
    プロジェクトルート配下に.envファイルを作成
ANCHOR_PROVIDER_URL="https://api.devnet.solana.com"
ANCHOR_WALLET="/Users/<User>/.config/solana/id.json"
  • scripts/mint-sbt.tsを作成し、以下のコードを追加する
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { MySbtProject } from "../target/types/my_sbt_project";
import dotenv from "dotenv";
// .envファイルを読み込む
dotenv.config();

async function main() {
  // プロバイダーの設定
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  // プログラムの取得
  const program = anchor.workspace.MySbtProject as Program<MySbtProject>;

  // 新しいSBTアカウントの生成
  const sbtAccount = anchor.web3.Keypair.generate();
  const testData = "My First SBT";

  try {
    // SBTの発行
    const tx = await program.methods.initialize(testData)
      .accounts({
        sbtAccount: sbtAccount.publicKey,
        user: provider.wallet.publicKey,
      })
      .signers([sbtAccount])
      .rpc();

    console.log("SBT発行成功!");
    console.log("Transaction:", tx);
    console.log("SBT Account:", sbtAccount.publicKey.toString());

    // 発行したSBTの内容を確認
    const account = await program.account.sbtAccount.fetch(sbtAccount.publicKey);
    console.log("SBT Data:", account);

  } catch (error) {
    console.error("エラー:", error);
  }
}

main().then(
  () => process.exit(),
  err => {
    console.error(err);
    process.exit(-1);
  }
); 

テストコードの実装

SBTを発行する前にテストコードでSBT発行できるかテストする。
tests/mint-sbt.tsを作成。以下のコードを追加する。

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { MySbtProject } from "../target/types/my_sbt_project";
import { expect } from "chai";
import assert from "assert";

describe("mint-sbt", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  const program = anchor.workspace.MySbtProject as Program<MySbtProject>;
  const sbtAccount = anchor.web3.Keypair.generate();

  it("Mint new SBT", async () => {
    const sbtData = "My First SBT Token";
    
    try {
      const tx = await program.methods
        .initialize(sbtData)
        .accounts({
          sbtAccount: sbtAccount.publicKey,
          user: provider.wallet.publicKey,
        })
        .signers([sbtAccount])
        .rpc();

      // トランザクションの確認を待つ
      await provider.connection.confirmTransaction(tx);
      console.log("SBT発行トランザクション:", tx);

      // 少し待機してアカウントの状態が更新されるのを待つ
      await new Promise(resolve => setTimeout(resolve, 2000));

      // SBTアカウントの状態を確認
      const account = await program.account.sbtAccount.fetch(sbtAccount.publicKey);
      console.log("発行されたSBT:", {
        data: account.data,
        owner: account.owner.toString(),
        isTransferable: account.isTransferable,
      });

      expect(account.data).to.equal(sbtData);
      expect(account.owner.toString()).to.equal(provider.wallet.publicKey.toString());
      expect(account.isTransferable).to.equal(false);

    } catch (error) {
      console.error("エラー:", error);
      throw error;
    }
  });
  // 転送できないSBTを転送しようとした場合のテスト
  it("Cannot transfer non-transferable SBT", async () => {
    const newOwner = anchor.web3.Keypair.generate();
    
    try {
        // 転送できないSBTを転送
        await program.methods
            // 転送
            .transfer()
            // アカウント
            .accounts({
                sbtAccount: sbtAccount.publicKey,
                currentOwner: provider.wallet.publicKey,
                newOwner: newOwner.publicKey,
            })
            // 署名
            .rpc();
        assert.fail("転送失敗");
    } catch (error) {
        expect(error.message).to.include("This SBT is non-transferable");
    }
  });
});

テストの実行

テストがパスすればOK。

 mint-sbt % anchor test

...
  mint-sbt
SBT発行トランザクション: 3rUVeKEmdGXyrJpQ6SeAYfRGYWxHHJ3yhgk49inwph9DPbeyfJHBbjcsApYCWz6HzM6LYfaaVjjUUAqvJoST57rs
発行されたSBT: {
  data: 'My First SBT Token',
  owner: '573MBhYX7aQXKgMfVb656Wt7PDs6vAw4mRCndmkPL5pg',
  isTransferable: false
}
    ✔ Mint new SBT (3288ms)
    ✔ Cannot transfer non-transferable SBT (222ms)


  2 passing (4s)

✨  Done in 5.18s.
Error: No such file or directory (os error 2)

SBTの発行

ではテストも通ったので、実際にSBTを発行(mint)してみましょう!

  • package.jsonに以下の記述を追加する
  "scripts": {
    "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
    "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check",
    "mint": "ts-node scripts/mint-sbt.ts", // 追加
    "fetch": "ts-node scripts/fetch-sbt.ts" // 後述
  },
  • SBTの発行(npmの場合はnpm run mint
mint-sbt % yarn mint
...
$ ts-node scripts/mint-sbt.ts
SBT発行成功!
Transaction: 5ak16ZSYWcVwNuxUDrGmfVZ2GGKK2HpGGQWc7Qw1ZnXynuogadnQujFE3DksPMYqt9u5fwKAFvT7u7yYx9CbHvaR
SBT Account: 3jJjsm1yT1ZSYBwimrNFTtuQh6tuHDRNx9SCsp7uMuX5
SBT Data: {
  data: 'My First SBT',
  owner: PublicKey [PublicKey(573MBhYX7aQXKgMfVb656Wt7PDs6vAw4mRCndmkPL5pg)] {
    _bn: <BN: 3cfae330a5026a7e1491eff7a12817f221101b74ecbbb038b9736a4ed910b995>
  },
  isTransferable: false
}
✨  Done in 1.93s.

署名しているプログラムID(Assigned Program Id)を確認すると、devnetへデプロイしたプログラムアカウントのIDになっていることが確認できます。solana explorer
スクリーンショット 2024-12-05 18.00.47.png

発行したSBTの確認

デプロイしたプログラムや発行したSBTは本来Verifyという検証を行って確認しますが、初心者ということもありVerifyがうまくいかずでしたので、発行したSBTをフェッチしてデータの中身を確認してみます。

  • scripts/fetch-sbt.tsを作成し、以下のコードを追加する
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { MySbtProject } from "../target/types/my_sbt_project";
import dotenv from "dotenv";
dotenv.config();

async function main() {
  // 環境変数からプロバイダーを取得
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  // プログラムをワークスペースに設定
  const program = anchor.workspace.MySbtProject as Program<MySbtProject>;
  
  // ここに確認したいSBTアカウントのアドレスを入れる
  const sbtAddress = new anchor.web3.PublicKey("3jJjsm1yT1ZSYBwimrNFTtuQh6tuHDRNx9SCsp7uMuX5");

  try {
    const account = await program.account.sbtAccount.fetch(sbtAddress);
    console.log("SBTの詳細:");
    console.log("データ:", account.data);
    console.log("所有者:", account.owner.toString());
    console.log("転送可能:", account.isTransferable);
  } catch (error) {
    console.error("エラー:", error);
  }
}

main().then(
  () => process.exit(),
  err => {
    console.error(err);
    process.exit(-1);
  }
); 
  • package.jsonにすでに記述しているので、コマンドでyarn fetchまたはnpm run fetchを実行する。

以下のように出ればOK

$ ts-node scripts/fetch-sbt.ts
SBTの詳細:
データ: My First SBT
所有者: 573MBhYX7aQXKgMfVb656Wt7PDs6vAw4mRCndmkPL5pg
転送可能: false
✨  Done in 1.44s.

以上でSBT発行の実装は終わりになります!

まとめ

この記事では、Solanaブロックチェーン上でSBT(Soulbound Token)を発行するプログラムの実装を行いました。主なポイントは以下の通りです:

1. SBTの特徴理解

  • NFTの一種であり、転送不可能なトークン
  • 学歴や資格など、個人に紐づく証明に適している

2. 開発環境の構築

  • Rust、Solana CLI、Anchorフレームワークのセットアップ
  • devnet環境での開発・テスト

3. 実装のポイント

  • RustでSBTの基本機能(発行・転送制限)を実装
  • TypeScriptでクライアント側の処理を実装
  • テストコードによる動作確認

4. 実際の動作確認

  • devnetへのデプロイ
  • SBTの発行と確認
  • 転送不可能な仕様の確認

 今回の実装を通じて、ブロックチェーン上でのSBT発行の基本的な流れと、Anchorフレームワークを利用したSolanaブロックチェーン開発を学びました。今回はシンプルなものですが、今回学んだプログラムを応用すれば、複雑なロジックも記述できるようになると思います!

最後まで読んでいただきありがとうございました!良いお年を!!

12
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?