solana/docs/src/proposals/cross-program-invocation.md

4.6 KiB

Cross-Program Invocation

Problem

In today's implementation a client can create a transaction that modifies two accounts, each owned by a separate on-chain program:

let message = Message::new(vec![
    token_instruction::pay(&alice_pubkey),
    acme_instruction::launch_missiles(&bob_pubkey),
]);
client.send_message(&[&alice_keypair, &bob_keypair], &message);

The current implementation does not, however, allow the acme program to conveniently invoke token instructions on the client's behalf:

let message = Message::new(vec![
    acme_instruction::pay_and_launch_missiles(&alice_pubkey, &bob_pubkey),
]);
client.send_message(&[&alice_keypair, &bob_keypair], &message);

Currently, there is no way to create instruction pay_and_launch_missiles that executes token_instruction::pay from the acme program. The workaround is to extend the acme program with the implementation of the token program, and create token accounts with ACME_PROGRAM_ID, which the acme program is permitted to modify. With that workaround, acme can modify token-like accounts created by the acme program, but not token accounts created by the token program.

Proposed Solution

The goal of this design is to modify Solana's runtime such that an on-chain program can invoke an instruction from another program.

Given two on-chain programs token and acme, each implementing instructions pay() and launch_missiles() respectively, we would ideally like to implement the acme module with a call to a function defined in the token module:

use token;

fn launch_missiles(keyed_accounts: &[KeyedAccount]) -> Result<()> {
    ...
}

fn pay_and_launch_missiles(keyed_accounts: &[KeyedAccount]) -> Result<()> {
    token::pay(&keyed_accounts[1..])?;

    launch_missiles(keyed_accounts)?;
}

The above code would require that the token crate be dynamically linked, so that a custom linker could intercept calls and validate accesses to keyed_accounts. That is, even though the client intends to modify both token and acme accounts, only token program is permitted to modify the token account, and only the acme program is permitted to modify the acme account.

Backing off from that ideal cross-program call, a slightly more verbose solution is to expose token's existing process_instruction() entrypoint to the acme program:

use token_instruction;

fn launch_missiles(keyed_accounts: &[KeyedAccount]) -> Result<()> {
    ...
}

fn pay_and_launch_missiles(keyed_accounts: &[KeyedAccount]) -> Result<()> {
    let alice_pubkey = keyed_accounts[1].key;
    let instruction = token_instruction::pay(&alice_pubkey);
    process_instruction(&instruction)?;

    launch_missiles(keyed_accounts)?;
}

where process_instruction() is built into Solana's runtime and responsible for routing the given instruction to the token program via the instruction's program_id field. Before invoking pay(), the runtime must also ensure that acme didn't modify any accounts owned by token. It does this by calling runtime::verify_account_changes() and then afterward updating all the pre_* variables to tentatively commit acme's account modifications. After pay() completes, the runtime must again ensure that token didn't modify any accounts owned by acme. It should call verify_account_changes() again, but this time with the token program ID. Lastly, after pay_and_launch_missiles() completes, the runtime must call verify_account_changes() one more time, where it normally would, but using all updated pre_* variables. If executing pay_and_launch_missiles() up to pay() made no invalid account changes, pay() made no invalid changes, and executing from pay() until pay_and_launch_missiles() returns made no invalid changes, then the runtime can transitively assume pay_and_launch_missiles() as whole made no invalid account changes, and therefore commit all account modifications.

Setting KeyedAccount.is_signer

When process_instruction() is invoked, the runtime must create a new KeyedAccounts parameter using the signatures from the original transaction data. Since the token program is immutable and existed on-chain prior to the acme program, the runtime can safely treat the transaction signature as a signature of a transaction with a token instruction. When the runtime sees the given instruction references alice_pubkey, it looks up the key in the transaction to see if that key corresponds to a transaction signature. In this case it does and so sets KeyedAccount.is_signer, thereby authorizing the token program to modify Alice's account.