Вызовы между программами

Межпрограммные вызовы

Среда выполнения Solana позволяет программам вызывать друг друга с помощью механизма, называемого межпрограммным вызовом. Вызов между программами достигается тем, что одна программа вызывает инструкцию другой. Вызывающая программа останавливается до тех пор, пока вызванная программа не завершит обработку инструкции.

Например, клиент может создать транзакцию, которая изменяет две учетные записи, каждая из которых принадлежит отдельным программам в сети:

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

Вместо этого клиент может разрешить программе acme удобно вызывать инструкции токена от имени клиента:

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

Имея две ончейн-программы token и acme, каждая из которых реализует инструкции pay() и launch_missiles() соответственно, acme можно реализовать с помощью вызова функции, определенной в модуле token, путем выдачи межпрограммный вызов:

mod acme {
    use token_instruction;

    fn launch_missiles(accounts: &[AccountInfo]) -> Result<()> {
        ...
    }

    fn pay_and_launch_missiles(accounts: &[AccountInfo]) -> Result<()> {
        let alice_pubkey = accounts[1].key;
        let instruction = token_instruction::pay(&alice_pubkey);
        invoke(&instruction, accounts)?;

        launch_missiles(accounts)?;
    }

invoke() встроен в среду выполнения Solana и отвечает за маршрутизацию данной инструкции в программу token через поле program_id инструкции.

Обратите внимание, что invoke требует, чтобы вызывающая сторона передала все учетные записи, требуемые вызываемой инструкцией, за исключением исполняемой учетной записи (program_id).

Прежде чем вызывать pay(), среда выполнения должна убедиться, что acme не изменил ни одной учетной записи, принадлежащей token. Он делает это, применяя политику среды выполнения к текущему состоянию учетных записей в момент вызова acme invoke по сравнению с начальным состоянием учетных записей в начале инструкции acme. После завершения pay() среда выполнения снова должна убедиться, что token не изменил ни одну учетную запись, принадлежащую acme, снова применив политику среды выполнения, но на этот раз с идентификатором программы token. Наконец, после завершения pay_and_launch_missiles() среда выполнения должна применить политику выполнения еще раз, как обычно, но с использованием всех обновленных переменных pre_*. Если выполнение pay_and_launch_missiles() до pay() не привело к недействительным изменениям учетной записи, pay() не произвело недопустимых изменений, и выполнение от pay() до возврата pay_and_launch_missiles() не произвело недопустимых изменений, тогда среда выполнения может транзитивно предположить, что pay_and_launch_missiles() в целом не внесла недопустимых изменений учетной записи, и, следовательно, зафиксирует все эти модификации учетной записи.

Инструкции, требующие привилегий

Среда выполнения использует привилегии, предоставленные вызывающей программе, чтобы определить, какие привилегии могут быть расширены для вызываемой программы. Привилегии в этом контексте относятся к подписавшим и записываемым учетным записям. Например, если инструкция, которую обрабатывает вызывающая сторона, содержит подписывающую или записываемую учетную запись, то вызывающая сторона может вызвать инструкцию, которая также содержит эту подписывающую и/или записываемую учетную запись.

Это расширение привилегий основано на том факте, что программы неизменяемы, за исключением особого случая обновления программы.

В случае с программой «acme» среда выполнения может безопасно рассматривать подпись транзакции как подпись инструкции «токен». Когда среда выполнения видит, что инструкция «token» ссылается на «alice_pubkey», она ищет ключ в инструкции «acme», чтобы определить, соответствует ли этот ключ подписанной учетной записи. В этом случае он делает это и тем самым разрешает программе «токен» модифицировать учетную запись Алисы.

Аккаунты, подписанные программой

Программы могут выдавать инструкции, содержащие подписанные учетные записи, которые не были подписаны в исходной транзакции, с помощью Программно-производных адресов.

Чтобы подписать учетную запись с адресами, производными от программы, программа может вызвать invoke_signed().

        invoke_signed(
            &instruction,
            accounts,
            &[&["First addresses seed"],
              &["Second addresses first seed", "Second addresses second seed"]],
        )?;

Глубина вызова

Вызовы между программами позволяют программам напрямую вызывать другие программы, но в настоящее время глубина ограничена 4.

Повторный вход

Повторный вход в настоящее время ограничен прямой саморекурсией, ограниченной фиксированной глубиной. Это ограничение предотвращает ситуации, когда программа может вызывать другую программу из промежуточного состояния, не зная, что позже она может быть вызвана обратно. Прямая рекурсия дает программе полный контроль над своим состоянием в момент обратного вызова.

Адреса, производные от программы

Адреса, производные от программы, позволяют использовать программно сгенерированные подписи при вызовах между программами.

Используя адрес, производный от программы, программа может получить права доступа к учетной записи, а затем передать эти права другому пользователю. Это возможно, потому что программа может действовать как подписывающая сторона в транзакции, которая дает полномочия.

Например, если два пользователя хотят сделать ставку на исход игры в Солане, каждый из них должен передать активы своей ставки какому-либо посреднику, который будет соблюдать их соглашение. В настоящее время нет возможности реализовать этого посредника как программу в Солане, потому что программа посредника не может передать активы победителю.

Эта возможность необходима для многих приложений DeFi, поскольку они требуют передачи активов агенту условного депонирования до тех пор, пока не произойдет какое-либо событие, определяющее нового владельца.

Адрес, производный от программы:

  1. Разрешить программам управлять определенными адресами, называемыми программными адресами, таким образом, чтобы ни один внешний пользователь не мог генерировать действительные транзакции с подписями для этих адресов.

  2. Разрешить программам программно подписывать адреса программ, присутствующие в инструкциях, вызываемых с помощью кросс-программных вызовов.

С учетом этих двух условий пользователи могут безопасно передавать или назначать полномочия сетевых активов на адреса программ, а затем программа может назначать эти полномочия в другом месте по своему усмотрению.

Закрытые ключи для программных адресов

Адрес программы не лежит на кривой ed25519 и, следовательно, не имеет связанного с ним действительного закрытого ключа, поэтому создание подписи для него невозможно. Хотя у него нет собственного закрытого ключа, он может использоваться программой для выдачи инструкции, включающей адрес Программы в качестве подписавшего.

Адреса программ, сгенерированные на основе хэша

Адреса программ детерминистически выводятся из набора начальных значений и идентификатор программы с использованием 256-битной хэш-функции, устойчивой к прообразу. Адрес программы не должен лежать на кривой ed25519, чтобы гарантировать отсутствие связанного закрытого ключа. Во время генерации будет возвращена ошибка, если будет обнаружено, что адрес лежит на кривой. Вероятность того, что это произойдет для данной коллекции семян и идентификатора программы, составляет примерно 50/50. Если это происходит, можно использовать другой набор начальных значений или начальное значение (дополнительное 8-битное начальное число), чтобы найти допустимый программный адрес вне кривой.

Детерминированные программные адреса для программ следуют тому же пути вывода, что и учетные записи, созданные с помощью SystemInstruction::CreateAccountWithSeed, который реализован с помощью Pubkey::create_with_seed.

Для справки, реализация выглядит следующим образом:

pub fn create_with_seed(
    base: &Pubkey,
    seed: &str,
    program_id: &Pubkey,
) -> Result<Pubkey, SystemError> {
    if seed.len() > MAX_ADDRESS_SEED_LEN {
        return Err(SystemError::MaxSeedLengthExceeded);
    }

    Ok(Pubkey::new(
        hashv(&[base.as_ref(), seed.as_ref(), program_id.as_ref()]).as_ref(),
    ))
}

Программы могут детерминировано получить любое количество адресов, используя начальные значения. Эти семена могут символически идентифицировать, как используются адреса.

Из Pubkey::

/// Generate a derived program address
///     * seeds, symbolic keywords used to derive the key
///     * program_id, program that the address is derived for
pub fn create_program_address(
    seeds: &[&[u8]],
    program_id: &Pubkey,
) -> Result<Pubkey, PubkeyError>

/// Find a valid off-curve derived program address and its bump seed
///     * seeds, symbolic keywords used to derive the key
///     * program_id, program that the address is derived for
pub fn find_program_address(
    seeds: &[&[u8]],
    program_id: &Pubkey,
) -> Option<(Pubkey, u8)> {
    let mut bump_seed = [std::u8::MAX];
    for _ in 0..std::u8::MAX {
        let mut seeds_with_bump = seeds.to_vec();
        seeds_with_bump.push(&bump_seed);
        if let Ok(address) = create_program_address(&seeds_with_bump, program_id) {
            return Some((address, bump_seed[0]));
        }
        bump_seed[0] -= 1;
    }
    None
}

Использование программных адресов

Клиенты могут использовать функцию create_program_address для создания адреса назначения. В этом примере мы предполагаем, что create_program_address(&[&["escrow"]], &escrow_program_id) создает действительный программный адрес, который не соответствует действительности.

// deterministically derive the escrow key
let escrow_pubkey = create_program_address(&[&["escrow"]], &escrow_program_id);

// construct a transfer message using that key
let message = Message::new(vec![
    token_instruction::transfer(&alice_pubkey, &escrow_pubkey, 1),
]);

// process the message which transfer one 1 token to the escrow
client.send_and_confirm_message(&[&alice_keypair], &message);

Программы могут использовать одну и ту же функцию для генерации одного и того же адреса. В приведенной ниже функции программа выдает token_instruction::transfer с адреса программы, как если бы у нее был закрытый ключ для подписи транзакции.

fn transfer_one_token_from_escrow(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
) -> ProgramResult {
    // User supplies the destination
    let alice_pubkey = keyed_accounts[1].unsigned_key();

    // Deterministically derive the escrow pubkey.
    let escrow_pubkey = create_program_address(&[&["escrow"]], program_id);

    // Create the transfer instruction
    let instruction = token_instruction::transfer(&escrow_pubkey, &alice_pubkey, 1);

    // The runtime deterministically derives the key from the currently
    // executing program ID and the supplied keywords.
    // If the derived address matches a key marked as signed in the instruction
    // then that key is accepted as signed.
    invoke_signed(&instruction, accounts, &[&["escrow"]])
}

Обратите внимание, что адрес, сгенерированный с помощью create_program_address, не обязательно будет допустимым программным адресом, выходящим за рамки кривой. Например, предположим, что начальное значение "escrow2" не генерирует действительный программный адрес.

Чтобы сгенерировать допустимый программный адрес, используя "escrow2 в качестве начального значения, используйте find_program_address, перебирая возможные начальные значения до тех пор, пока не будет найдена действительная комбинация. Предыдущий пример становится таким:

// find the escrow key and valid bump seed
let (escrow_pubkey2, escrow_bump_seed) = find_program_address(&[&["escrow2"]], &escrow_program_id);

// construct a transfer message using that key
let message = Message::new(vec![
    token_instruction::transfer(&alice_pubkey, &escrow_pubkey2, 1),
]);

// process the message which transfer one 1 token to the escrow
client.send_and_confirm_message(&[&alice_keypair], &message);

В программе это становится:

fn transfer_one_token_from_escrow2(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
) -> ProgramResult {
    // User supplies the destination
    let alice_pubkey = keyed_accounts[1].unsigned_key();

    // Iteratively derive the escrow pubkey
    let (escrow_pubkey2, bump_seed) = find_program_address(&[&["escrow2"]], program_id);

    // Create the transfer instruction
    let instruction = token_instruction::transfer(&escrow_pubkey2, &alice_pubkey, 1);

    // Include the generated bump seed to the list of all seeds
    invoke_signed(&instruction, accounts, &[&["escrow2", &[bump_seed]]])
}

Поскольку find_program_address требует повторения ряда вызовов create_program_address, он может использовать больше вычислительного бюджета при использовании в цепочке. Чтобы снизить стоимость вычислений, используйте find_program_address вне цепочки и передайте полученное начальное значение в программу.

Инструкции, требующие подписантов

Адреса, сгенерированные с помощью create_program_address и find_program_address, неотличимы от любого другого открытого ключа. Единственный способ для среды выполнения проверить, что адрес принадлежит программе, — предоставить программе начальные значения, используемые для генерации адреса.

Среда выполнения вызовет create_program_address и сравнит результат с адресами, указанными в инструкции.

Примеры

См. [Разработка на Rust](разработка/программы в сети/../../../программы в сети/разработка-rust.md#examples) и [Разработка на C](разработка/в сети). chain-programs/../../../on-chain-programs/developing-c.md#examples) для примеров того, как использовать межпрограммный вызов.