hanzochang
hanzochang
はじめに
対象読者
remaining_accountsの基本理解
remaining_accountsが必要になるケース
例1: DEXでのマルチホップスワップ
例2: NFTマーケットプレイスの一括処理
例3: ゲームのマルチプレイヤー処理
なぜ可変長アカウントが必要なのか?
固定アカウント数の限界
Anchorの`'info`ライフタイムについて
remaining_accountsの技術的定義
典型的な使用パターン
ライフタイム問題の本質
問題が発生するメカニズム
ライフタイムスコープの視覚化
Rustボローチェッカーの視点
ボローチェッカーの分析プロセス
エラーメッセージの解読
解決パターン
パターン1: Clone/Copy アプローチ
パターン2: 構造化アクセス
パターン3: 関数分割アプローチ
まとめ
重要なポイント
参考リンク
https://s3.ap-northeast-1.amazonaws.com/hanzochang.com/_v2/1753932341187_tpjnqb7ghi8.png

Solana x Anchor - remaining_accounts CPI利用時のlifetimeエラー解決

Solana remaining_accounts利用時のlifetimeエラーを解決。CPI呼び出しでのライフタイム競合の原因と3つの解決パターンを実コード付きで解説。

公開日2025.07.31

更新日2025.07.31

はじめに

Anchorでsolana programを開発していると、remaining_accountsを使った動的アカウント処理で遭遇するのがRustのライフタイムエラーです。

error[E0716]: temporary value dropped while borrowed
error: lifetime may not live long enough

このエラーは単なるコンパイルエラーではなく、Rustの所有権システムとAnchorの設計が交差する部分で発生する問題です。本記事では、解決方法を提供します。

対象読者

  • Anchorでのsolana開発をしている方
  • remaining_accountsを使った動的アカウント処理を実装したい方
  • RustのlifetimeとBorrow checkerの基本概念を理解している方

remaining_accountsの基本理解

remaining_accountsが必要になるケース

remaining_accountsは、コンパイル時にアカウント数が確定しない動的なアカウント処理で使用されます。例えば以下のような場面で必要になります。

例1: DEXでのマルチホップスワップ

ルートによってプール数が変わる場合の例

pub fn multi_hop_swap(
    ctx: Context<SwapAccounts>,
    route_plan: Vec<SwapStep>
) -> Result<()> {
    // remaining_accounts = [pool1, pool2, pool3, token_a, token_b, ...]
    // ルートによって2個〜10個以上のプールを通過

    for (i, step) in route_plan.iter().enumerate() {
        let pool_account = &ctx.remaining_accounts[i];
        // 各プールでスワップ処理...
    }
}

例2: NFTマーケットプレイスの一括処理

複数NFTを1トランザクションで処理する場合

pub fn bulk_transfer(
    ctx: Context<BulkTransfer>,
    nft_list: Vec<NftData>
) -> Result<()> {
    // remaining_accounts = [nft1_account, nft2_account, nft3_account, ...]
    // 処理数によって可変(1個〜35個程度まで)

    for (i, nft_data) in nft_list.iter().enumerate() {
        let nft_account = &ctx.remaining_accounts[i];
        // 各NFTを処理...
    }
}

例3: ゲームのマルチプレイヤー処理

参加者数が動的に変わる場合

pub fn distribute_rewards(
    ctx: Context<GameRewards>,
    participants: Vec<PlayerData>
) -> Result<()> {
    // remaining_accounts = [player1, player2, player3, ...]
    // 参加プレイヤー数によって可変

    for (participant, account_info) in participants.iter().zip(ctx.remaining_accounts.iter()) {
        // 各プレイヤーに報酬を配布...
    }
}

なぜ可変長アカウントが必要なのか?

固定アカウント数の限界

従来のAnchor Accounts構造体では、アカウント数をコンパイル時に確定する必要があります。これにより複数の同様な処理で非効率が生じます。

// ❌ 非効率:各NFTごとに別トランザクション
transfer_nft(nft1) // Transaction 1 + 手数料
transfer_nft(nft2) // Transaction 2 + 手数料
transfer_nft(nft3) // Transaction 3 + 手数料

// ✅ 効率的:1トランザクションで完結
bulk_transfer([nft1, nft2, nft3]) // Transaction 1のみ

Anchorの'infoライフタイムについて

remaining_accountsの問題を理解する前に、Anchorの'infoライフタイムについて理解する必要があります:

// Anchorの基本構造
#[derive(Accounts)]
pub struct MyAccounts<'info> {  // ← 'info ライフタイムパラメータ
    pub user: Signer<'info>,
    pub token_account: Account<'info, TokenAccount>,
}

pub fn my_instruction(ctx: Context<MyAccounts>) -> Result<()> {
    // ctx.accounts 内のすべてのアカウントは 'info ライフタイムを持つ
    // この 'info は命令の実行期間全体にわたって有効
}

'infoライフタイムは以下の特徴があります:

  • 命令スコープ全体: 命令の開始から終了まで一貫して有効
  • アカウント参照の基準: すべてのアカウント参照がこのライフタイムに束縛される
  • CPI要求: Cross-Program Invocation時にも同じライフタイムが要求される

remaining_accountsの技術的定義

remaining_accountsは、AnchorのContext構造体で提供される動的アカウント配列です。明示的に定義されていないアカウントを実行時に受け取ることができます:

pub struct Context<'a, 'b, 'c, 'info, T> {
    pub remaining_accounts: &'c [AccountInfo<'info>], // ← 'info ライフタイム
    // その他のフィールド...
}

注目すべき点は、remaining_accountsの各要素も'infoライフタイムを持つことです。これが後述するライフタイム問題の鍵となります。

実際の使用例では、明示的なアカウント構造体と組み合わせて動的アカウントを処理します。

#[derive(Accounts)]
pub struct DynamicAccounts<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    pub system_program: Program<'info, System>,
    // 動的に追加されるアカウントは remaining_accounts から取得
}

pub fn process_dynamic_accounts(
    ctx: Context<DynamicAccounts>,
    target_keys: Vec<Pubkey>,
) -> Result<()> {
    // remaining_accounts を使って動的にアカウントを処理
    for (i, target_key) in target_keys.iter().enumerate() {
        let account_info = &ctx.remaining_accounts[i];
        // アカウント処理...
    }
    Ok(())
}

典型的な使用パターン

remaining_accountsへのアクセス方法は主に3つのパターンがあります。それぞれ異なるユースケースに適しています:

// パターン1: インデックスベースアクセス
let token_account = &ctx.remaining_accounts[0];

// パターン2: 検索ベースアクセス
let account_info = ctx.remaining_accounts
    .iter()
    .find(|acc| acc.key == &target_key)
    .ok_or(CustomError::AccountNotFound)?;

// パターン3: 条件付きフィルタリング
let valid_accounts: Vec<_> = ctx.remaining_accounts
    .iter()
    .filter(|acc| acc.is_signer)
    .collect();

ライフタイム問題の本質

問題が発生するメカニズム

remaining_accountsから取得したアカウント情報は、イテレータのライフタイムに束縛されます。しかし、CPIコールでは、より長いライフタイムを持つアカウント情報が必要になることが多く、ここでライフタイム競合が発生します。

pub fn problematic_cpi_call(ctx: Context<DynamicAccounts>) -> Result<()> {
    // これは失敗する
    let account_info = ctx.remaining_accounts
        .iter()
        .find(|acc| acc.key == &target_key)?; // イテレータのライフタイム

    // CPIコールでaccount_infoを使用しようとするとライフタイムエラー
    token::transfer(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            token::Transfer {
                from: account_info, // ❌ ライフタイムエラー
                to: ctx.accounts.destination.to_account_info(),
                authority: ctx.accounts.authority.to_account_info(),
            },
        ),
        amount,
    )?;

    Ok(())
}

ライフタイムスコープの視覚化

ステップコード行iteraccount_infoCPI要求
1
let iter = remaining_accounts.iter()
✅ 生存
-
-
2
let account_info = iter.find(...)
✅ 生存
✅ 生存
-
3
; (文の終了)
❌ 破棄
❌ 無効化
-
4
token::transfer(account_info, ...)
-
❌ 使用不可
✅ 必要

問題: account_infoが無効化された後にCPIで使用しようとしている

解決策: account_infoiterより長く生存させる仕組みが必要

Rustのボローチェッカーは、account_infoの参照がイテレータのスコープ内でしか有効でないことを検出し、CPIコールで使用することを拒否します。

Rustボローチェッカーの視点

ボローチェッカーの分析プロセス

// ボローチェッカーの分析例
pub fn analyze_lifetimes(ctx: Context<DynamicAccounts>) -> Result<()> {
    // 1. remaining_accounts は 'info ライフタイムを持つ
    // 2. iter() は remaining_accounts を借用する
    let iter = ctx.remaining_accounts.iter(); // 借用開始

    // 3. find() はイテレータの要素への参照を返す
    let account_info = iter.find(|acc| acc.key == &target_key)?;

    // 4. この時点で account_info のライフタイムは iter に依存
    // 5. CPIコールは 'info と同等のライフタイムを要求
    // 6. しかし account_info は iter の短いライフタイムしか持たない
    // → ライフタイム競合!
}

上記のコードを表で可視化すると、ライフタイム競合のタイミングが明確になります:

ステップコード行remaining_accountsiteraccount_infoCPI要求
1
ctx.remaining_accounts
✅ 'info生存
-
-
-
2
let iter = ctx.remaining_accounts.iter()
✅ 'info生存
✅ 借用開始
-
-
3
let account_info = iter.find(...)
✅ 'info生存
✅ 生存
✅ iterに依存
-
4
; (文の終了)
✅ 'info生存
❌ 借用終了
❌ 無効化
-
5
token::transfer(account_info, ...)
✅ 'info生存
-
❌ 使用不可
✅ 'info必要

問題: account_infoiterのライフタイムに束縛され、CPI呼び出し時には無効になっている

解決が必要な点: account_infoiterより長く生存させる仕組み

エラーメッセージの解読

error[E0716]: temporary value dropped while borrowed
  --> src/lib.rs:XX:XX
   |
XX |     let account_info = ctx.remaining_accounts.iter()
   |                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ creates a temporary which is freed while still in use
XX |         .find(|acc| acc.key == &target_key)?;
   |                                              - temporary value is freed at the end of this statement
XX |
XX |     token::transfer(
   |     --------------- borrow later used here

このエラーは、一時的な値(イテレータ)が、その借用が必要な期間よりも早く破棄されることを示しています。

解決パターン

パターン1: Clone/Copy アプローチ

最も直接的な解決方法は、必要な情報をコピーまたはクローンすることです。

pub fn solution_clone_approach(
    ctx: Context<DynamicAccounts>,
    target_key: Pubkey,
) -> Result<()> {
    // アカウント情報を事前に検索・複製
    let account_pubkey = ctx.remaining_accounts
        .iter()
        .find(|acc| acc.key == &target_key)
        .map(|acc| *acc.key)
        .ok_or(CustomError::AccountNotFound)?;

    // インデックスも保存
    let account_index = ctx.remaining_accounts
        .iter()
        .position(|acc| acc.key == &target_key)
        .ok_or(CustomError::AccountNotFound)?;

    // 直接インデックスでアクセス(ライフタイム競合なし)
    let account_info = &ctx.remaining_accounts[account_index];

    // CPIコールが可能
    token::transfer(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            token::Transfer {
                from: account_info.clone(), // ✅ 成功
                to: ctx.accounts.destination.to_account_info(),
                authority: ctx.accounts.authority.to_account_info(),
            },
        ),
        amount,
    )?;

    Ok(())
}

メリット: シンプルで理解しやすい
デメリット: 若干のパフォーマンスオーバーヘッド

パターン2: 構造化アクセス

アカウントアクセスを構造化して、ライフタイム問題を回避します。

pub fn solution_structured_access(
    ctx: Context<DynamicAccounts>,
    operations: Vec<TokenOperation>,
) -> Result<()> {
    // 操作に必要なアカウントインデックスを事前計算
    let account_indices: Result<Vec<usize>> = operations
        .iter()
        .map(|op| {
            ctx.remaining_accounts
                .iter()
                .position(|acc| acc.key == &op.account_key)
                .ok_or(CustomError::AccountNotFound)
        })
        .collect();

    let indices = account_indices?;

    // 各操作を実行(ライフタイム競合なし)
    for (operation, &index) in operations.iter().zip(indices.iter()) {
        let account_info = &ctx.remaining_accounts[index];
        execute_token_operation(ctx, account_info, operation)?;
    }

    Ok(())
}

fn execute_token_operation(
    ctx: &Context<DynamicAccounts>,
    account_info: &AccountInfo,
    operation: &TokenOperation,
) -> Result<()> {
    // 個別の操作実行
    match operation.operation_type {
        OperationType::Transfer => {
            token::transfer(
                CpiContext::new(
                    ctx.accounts.token_program.to_account_info(),
                    token::Transfer {
                        from: account_info.clone(),
                        to: ctx.accounts.destination.to_account_info(),
                        authority: ctx.accounts.authority.to_account_info(),
                    },
                ),
                operation.amount,
            )?;
        }
        // 他の操作...
    }
    Ok(())
}

メリット: 大量の動的操作に対応、構造化されたコード
デメリット: 複雑性の増加

パターン3: 関数分割アプローチ

ライフタイムスコープを関数分割で制御します。

pub fn solution_function_split(
    ctx: Context<DynamicAccounts>,
    target_key: Pubkey,
) -> Result<()> {
    // アカウント検証フェーズ
    let account_index = find_account_index(&ctx.remaining_accounts, &target_key)?;

    // CPI実行フェーズ(分離された関数)
    execute_cpi_with_account(ctx, account_index)?;

    Ok(())
}

fn find_account_index(
    remaining_accounts: &[AccountInfo],
    target_key: &Pubkey,
) -> Result<usize> {
    remaining_accounts
        .iter()
        .position(|acc| acc.key == target_key)
        .ok_or_else(|| error!(CustomError::AccountNotFound))
}

fn execute_cpi_with_account(
    ctx: Context<DynamicAccounts>,
    account_index: usize,
) -> Result<()> {
    let account_info = &ctx.remaining_accounts[account_index];

    token::transfer(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            token::Transfer {
                from: account_info.clone(),
                to: ctx.accounts.destination.to_account_info(),
                authority: ctx.accounts.authority.to_account_info(),
            },
        ),
        TRANSFER_AMOUNT,
    )?;

    Ok(())
}

メリット: コードの可読性向上、テスト容易性
デメリット: 関数分割のオーバーヘッド

まとめ

remaining_accountsとRustライフタイムの問題は、Anchorでの高度な動的アカウント処理において避けて通れない課題です。本記事で紹介した解決パターンを理解し、適切に選択することで、この問題を克服できます。

重要なポイント

  1. ライフタイム競合の本質理解: イテレータのライフタイムとCPIコールの要求の不一致
  2. 適切な解決パターンの選択: Clone/Copy、構造化アクセス、関数分割から状況に応じて選択
  3. パフォーマンスと保守性のバランス: 単純さと効率性のトレードオフを考慮
  4. 包括的なテスト: ライフタイム問題が発生しないことの継続的な検証
💡 実装のヒント

ライフタイム問題に遭遇した場合、まずはClone/Copyアプローチから試してみることをお勧めします。ほとんどのケースで十分なパフォーマンスを提供し、コードの理解しやすさを保てます。

参考リンク

picture
hanzochang - 半澤勇大
慶應義塾大学卒業後、Webプランナーとして勤務。 ナショナルクライアントのキャンペーンサイトの企画・演出を担当。 その後開発会社に創業メンバーとして参加。 Fintech案件や大手企業のDXプロジェクトに関わり、その後個人事業主として独立し、 2023年にWeb3に特化した開発会社として法人化しました。 現在はWeb3アプリ開発を中心にAI開発フローの整備を行っています。
また、趣味で2017年ごろより匿名アカウントでCryptoの調査等を行い、 ブロックチェーンメディアやSNSでビットコイン論文等の図解等を発信していました。
X (Twitter)

お問い合わせはこちらから

ご希望に応じて職務経歴書や過去のポートフォリオを提出可能ですので、必要な方はお申し付けください。
また内容とによっては返信ができない場合や、お時間をいただく場合がございます。あらかじめご了承ください。