
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(())
}
ライフタイムスコープの視覚化
ステップ | コード行 | iter | account_info | CPI要求 |
---|---|---|---|---|
1 | let iter = remaining_accounts.iter() | ✅ 生存 | - | - |
2 | let account_info = iter.find(...) | ✅ 生存 | ✅ 生存 | - |
3 | ; (文の終了) | ❌ 破棄 | ❌ 無効化 | - |
4 | token::transfer(account_info, ...) | - | ❌ 使用不可 | ✅ 必要 |
問題: account_info
が無効化された後にCPIで使用しようとしている
解決策: account_info
をiter
より長く生存させる仕組みが必要
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_accounts | iter | account_info | CPI要求 |
---|---|---|---|---|---|
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_info
がiter
のライフタイムに束縛され、CPI呼び出し時には無効になっている
解決が必要な点: account_info
をiter
より長く生存させる仕組み
エラーメッセージの解読
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での高度な動的アカウント処理において避けて通れない課題です。本記事で紹介した解決パターンを理解し、適切に選択することで、この問題を克服できます。
重要なポイント
- ライフタイム競合の本質理解: イテレータのライフタイムとCPIコールの要求の不一致
- 適切な解決パターンの選択: Clone/Copy、構造化アクセス、関数分割から状況に応じて選択
- パフォーマンスと保守性のバランス: 単純さと効率性のトレードオフを考慮
- 包括的なテスト: ライフタイム問題が発生しないことの継続的な検証
💡 実装のヒント
ライフタイム問題に遭遇した場合、まずはClone/Copyアプローチから試してみることをお勧めします。ほとんどのケースで十分なパフォーマンスを提供し、コードの理解しやすさを保てます。
参考リンク
- Anchor公式ドキュメント - The Accounts Struct -
remaining_accounts
の基本概念 - GitHub - danmt/anchor-remaining-accounts -
remaining_accounts
の実装例集 - Solana Stack Exchange - remaining_accounts - 実践的な質問と回答
- Anchor Rust Docs - Context struct - Context構造体の詳細仕様
- GitHub Issue - Lifetime issue in using ctx.remaining_accounts - 実際のライフタイム問題の議論
お問い合わせはこちらから
ご希望に応じて職務経歴書や過去のポートフォリオを提出可能ですので、必要な方はお申し付けください。
また内容とによっては返信ができない場合や、お時間をいただく場合がございます。あらかじめご了承ください。