Global Trend Radar
Dev.to US tech 2026-06-27 01:00

クロスプログラム呼び出し:1つのSolanaプログラムが別のプログラムを呼び出す方法

原題: Cross-Program Invocations: How One Solana Program Calls Another

元記事を開く →

分析結果

カテゴリ
AI
重要度
59
トレンドスコア
21
要約
Solanaプログラム間の呼び出し(クロスプログラム呼び出し)は、異なるプログラムが相互に機能を利用するための重要なメカニズムです。これにより、開発者は複数のプログラムを連携させ、より複雑なアプリケーションを構築できます。具体的には、あるプログラムが別のプログラムの機能を呼び出す際の手順や注意点、セキュリティの考慮事項について解説します。
キーワード
Last time we ended on a promise. A Program Derived Address has no private key, so no human can sign for it, yet a program still needs to move tokens out of a vault it owns or release funds from an escrow. The mechanism that makes that work is the Cross-Program Invocation, and it's what this post is about. CPIs are also the feature that makes Solana composable. A program on its own is a closed box. A program that can call other programs can build on everything already deployed: the System Program to create accounts and move SOL, the Token Program to mint and transfer tokens, any other program someone has shipped. If you've ever wondered how a single Anchor instruction manages to transfer tokens when your program clearly doesn't contain transfer logic, the answer is a CPI into the Token Program. The problem: your program can't do everything itself Say your program runs a vault that holds SOL for a user, and later releases it under some condition. To move SOL between system-owned accounts, the transfer has to be performed by the System Program, since that's the program that owns those accounts and your program can only debit lamports from accounts it owns directly. So when the source is a system-owned account, your program needs a way to call the System Program mid-instruction. That's one wall. There's a second one as soon as the account being moved from is a PDA your program controls: authorizing the action requires a signature for an address that has no private key. CPIs solve the first wall, letting your program invoke another program's instruction. PDA signing, layered on top, solves the second. The same pattern shows up constantly with tokens, where your program's PDA is the authority over a token account and has to authorize a transfer through the Token Program, which is the example we'll build below. The idea: one instruction calls another program's instruction A Cross-Program Invocation is one program calling an instruction on another program during its own execution. The calling program (the caller) builds an instruction aimed at the callee program, hands it the accounts that instruction needs, and invokes it. The callee runs to completion, then control returns to the caller, which continues where it left off. The key thing to hold onto is that this is the same kind of instruction a normal client would send from outside. You are assembling a target program ID, an account list, and instruction data, exactly what a transaction contains, except your program is the one issuing it instead of a wallet. The runtime treats it as a nested call within the same transaction. There's a depth limit worth knowing: CPIs can only nest so far. Program A can call B, B can call C, and so on, but the runtime caps how deep the chain goes to keep execution bounded. The current limit is 5 levels (raised to 9 under a newer runtime change, SIMD-0268), and you'll rarely come close in practice. invoke and invoke_signed: the two functions At the native level, Solana gives you two functions for making a CPI. invoke is for calls where the signatures you need are already present on the transaction. The privileges of the accounts passed into your program extend to the program you call, so if a user signed the outer transaction, that signature carries through to the CPI. This is what you use to act on a normal user-owned account, like asking the System Program to transfer SOL the user already authorized. pub fn invoke ( instruction : & Instruction , account_infos : & [ AccountInfo < '_ > ], ) -> ProgramResult invoke_signed is for when your program needs a PDA to sign. Since the PDA has no private key, you instead pass the seeds (plus the bump) used to derive it. The runtime re-derives the address from those seeds and your program's ID, and if it matches an account in the call, it treats that account as having signed. pub fn invoke_signed ( instruction : & Instruction , account_infos : & [ AccountInfo < '_ > ], signers_seeds : & [ & [ & [ u8 ]]], ) -> ProgramResult A detail that demystifies the whole thing: invoke is just invoke_signed with an empty seeds array. They run through the same path; the only difference is whether you supply signer seeds. pub fn invoke ( instruction : & Instruction , account_infos : & [ AccountInfo ]) -> ProgramResult { invoke_signed ( instruction , account_infos , & []) } The part that matters most: PDA signing is not cryptographic This is the conceptual center of the post, so it's worth slowing down. When a normal account signs a transaction, a private key produces a cryptographic signature. A PDA has no private key, so nothing like that can happen. Instead, PDA signing is a runtime construct. You hand invoke_signed the seeds, the runtime re-derives the PDA from those seeds and the calling program's ID, and if the derived address matches an account in the call, the runtime marks that account as signed for the duration of the CPI. No signature is ever computed. The "signature" is the runtime agreeing that because your program supplied the correct seeds, and those seeds plus your program ID derive to this exact address, your program is allowed to authorize actions on it. This is the cash-out of everything from the PDA post: the address is derived from your program, so only your program can present the seeds that unlock it. The absence of a private key isn't a gap that PDA signing patches over. It's the whole reason the scheme is secure, because no external party can ever produce these credentials. How it looks in Anchor Anchor wraps the raw functions in a CpiContext , which bundles the program you're calling with the accounts that instruction needs. You build the context, then call the typed helper for the instruction. A plain CPI, transferring SOL via the System Program using the user's own signature. Here Transfer is the System Program's transfer, which takes just from and to : use anchor_lang :: system_program ::{ transfer , Transfer }; let cpi_context = CpiContext :: new ( ctx .accounts.system_program .to_account_info (), Transfer { from : ctx .accounts.sender .to_account_info (), to : ctx .accounts.recipient .to_account_info (), }, ); transfer ( cpi_context , amount ) ? ; A signed CPI, where a PDA is the authority. This one transfers SPL tokens through the Token Program, so the Transfer here is the token program's version, which also takes an authority , the account that must sign. That authority is our PDA, and new_with_signer plus the seeds is how the PDA signs: use anchor_spl :: token ::{ transfer , Transfer }; let bump = ctx .accounts.vault.bump ; let seeds : & [ & [ & [ u8 ]]] = & [ & [ b"vault" , authority_key .as_ref (), & [ bump ]]]; let cpi_context = CpiContext :: new_with_signer ( ctx .accounts.token_program .to_account_info (), Transfer { from : ctx .accounts.vault_token_account .to_account_info (), to : ctx .accounts.recipient_token_account .to_account_info (), authority : ctx .accounts.vault .to_account_info (), }, seeds , ); transfer ( cpi_context , amount ) ? ; Read the difference in plain terms. CpiContext::new says "make this call using the signatures already on the transaction." CpiContext::new_with_signer says "make this call, and also present these seeds so the runtime will let my PDA sign." The seeds you pass are the same ones you'd use to derive the PDA, with the bump as the final element. If you're calling another Anchor program rather than a built-in one, you add it as a dependency with the cpi feature, which generates typed instruction builders for it. That's a setup detail the lessons will walk through; the mental model is the same either way. A few things that trip people up Forgetting an account the callee needs. A CPI passes accounts to the callee, and the callee validates them just like any instruction. If the inner instruction needs the System Program or a token program account, it has to be in your account list and in your #[derive(Accounts)] struct. A missing account shows up as a failure inside the CPI, not in your own logic. Wrong seeds in invoke_signed . If the seeds you pass don't re-derive to the PDA the callee expects as a signer, the runtime won't mark it signed and the call fails. The seeds, their order, and the bump all have to match the derivation exactly, the same discipline as the PDA constraints. Reaching for invoke_signed when invoke would do. If the authority is a normal user who already signed the transaction, you don't need signer seeds. Signing with a PDA is only for accounts your program controls that have no key of their own. Confusing "who signs" with "who owns." A program can sign for a PDA derived from its own ID. It can't sign for arbitrary accounts just because they're passed in. The seeds prove the PDA belongs to the calling program, which is what authorizes the signature. TL;DR A CPI is one program calling another program's instruction mid-execution, which is what makes Solana composable. You build a target instruction, accounts, and data, much like a client would, and your program issues it. invoke uses signatures already on the transaction; invoke_signed lets a PDA sign by supplying its seeds. PDA signing isn't cryptographic: the runtime re-derives the PDA from the seeds and your program ID and marks it signed if it matches. In Anchor, CpiContext::new is a plain CPI and CpiContext::new_with_signer adds the PDA seeds. CPIs nest only so deep: the stack limit is 5 levels (9 under SIMD-0268). Going further The Solana CPI documentation covers both functions, privilege extension, and the execution flow in detail, and the quick-start CPI tutorial builds a vault that signs with a PDA end to end. For the native signature, the invoke_signed reference on docs.rs spells out exactly how the runtime treats PDA signing. If you're doing 100 Days of Solana, the next arc puts this to work, and since you already understand PDAs, the signing half should click rather than mystify. Not joined yet? It's not too late to build alongside everyone: mlh.link/solana-100 . L