Back to Blog

Безопасность экосистемы Solana (2) — Взаимодействие между программами

March 18, 2022
5 min read

0. Обзор

1. Обзор

В предыдущем блоге мы рассказали, как развертывать программу и взаимодействовать с ней. Помимо вызова инструкций программы со стороны клиента, Solana также позволяет программам вызывать друг друга через механизм, называемый межпрограммным вызовом (cross-program invocation). В этой статье мы проиллюстрируем, как используется межпрограммный вызов. Тестовый код находится здесь.

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

Межпрограммный вызов обычно реализуется с помощью функции invoke. В этом разделе мы проиллюстрируем использование invoke на примере перевода лампортов внутри программы.

В Solana существует родная программа под названием System Program, которая отвечает за создание новых аккаунтов и перевод лампортов (SOL). В данном случае, чтобы перевести лампорты внутри программы (например, Программы А), Программа А вызовет функцию transfer() в System Program. Давайте разберем конкретный пример ниже.

2.1 Обзор кода

С 3-й по 10-ю строки импортируются необходимые библиотеки. Обратите внимание, что функция invoke() находится в библиотеке solana_program::program.

В функции process_instruction строки с 22 по 30 извлекают три аккаунта, переданные клиентом. Обратите внимание, что лампорты будут переведены из from_account в to_account. Со строки 33 по 44 вызывается функция invoke(). Она принимает два аргумента. Первый — это целевая вызываемая инструкция, а второй — набор аккаунтов. В этом примере целевая инструкция — transfer(), которая используется для перевода лампортов. Аккаунты включают в себя все аккаунты, необходимые для вызываемой инструкции. В данном случае это from_account и to_account.

Развернутую программу можно найти по следующей ссылке.

https://explorer.solana.com/address/EPaLuYQ4c11BJAe9ucLbta3xGFb17Zy3cZh3UDPbXRG9?cluster=devnet

2.2 Перевод лампортов

Как уже упоминалось, мы отправляем транзакцию в развернутую программу. Развернутая программа затем вызовет System Program для перевода лампортов на целевой адрес. Приведенный выше код показывает, как клиент формирует транзакции. Строки с 93 по 101 извлекают programId развернутой программы. Строки с 103 по 109 генерируют адреса отправителя, получателя и System Program. Строки с 111 по 117 конструируют транзакцию. В строке 118 транзакция отправляется в кластер Solana. Обратите внимание, что единственный подписант в этой транзакции — отправитель, что задано в строке 112. Отправитель должен авторизовать (т.е. подписать) транзакцию, которая списывает деньги с его/ее счета, а получатель — нет (строка 113).

Транзакцию можно найти по следующей ссылке.

https://explorer.solana.com/tx/4cxqnff8SakVcE9y5phmh6utcbBUGaLDPvqMRgSy9aPGdNVH6DCsQyJXCCzRGvW5CpygUi5pqhgBQxczXnWCoqPJ?cluster=devnet

3. Invoke или Invoke_signed

В Solana программы могут генерировать аккаунты во время выполнения, которые называются производными адресами программ (PDA). Если целевая инструкция, вызываемая программой, содержит подписанные аккаунты с PDA, следует использовать invoke_signed() вместо invoke(). Мы используем другой пример, чтобы продемонстрировать использование invoke_signed(). Теперь давайте сначала разберем код контракта.

3.1 Обзор кода

В этом примере мы создаем PDA во время выполнения внутри программы (т.е. Программы B). Чтобы создать PDA, Программа B должна вызвать System Program.

Строки с 3 по 10 импортируют необходимые библиотеки. Обратите внимание, что в этот раз импортируется invoke_signed() (строка 6).

Как и в примере из раздела 2, мы извлекаем необходимые аккаунты (строки 22–27), которыми являются System Program и PDA. Затем мы используем функцию find_program_address() для генерации адреса PDA и начального числа (seed), используемого для вывода этого PDA за пределы кривой ed25519 (строки 29–30). Это гарантирует, что у адреса нет связанного закрытого ключа. В этом примере клиент и программа используют одно и то же число (т.е. 'You pass butter') для генерации PDA. Таким образом, открытые ключи должны совпадать, что проверяется в строках с 31 по 34. После этого мы вызываем invoke_signer для выполнения инструкции allocate() (строки 37–47). В отличие от функции invoke(), она принимает на один аргумент больше — это начальные числа (seeds), которые использовались для создания PDA, а также bump-seed, использованный функцией find_program_address().

Чтобы создать/выделить аккаунт, сам аккаунт должен предоставить подпись, и в этом примере PDA выступает в роли подписанта. Для подписи PDA используется invoke_signed. В частности, Solana будет использовать начальные числа и programId вызывающей стороны (т.е. Программы B), чтобы воссоздать PDA и сопоставить его с предоставленными аккаунтами (второй аргумент). Если они равны, PDA будет подписан.

Развернутую программу можно проверить ниже.

https://explorer.solana.com/address/4h3RXGsouTRvUWNG1Dqq2tuuASTXFebHC3wFzv3tSFCK?cluster=devnet

3.2 Создание PDA

Давайте разберем клиентский скрипт для второго примера.

В строке 104 мы извлекаем PDA и bump-seed с помощью функции findProgramAddress(). Со строки 112 по 120 создается и отправляется транзакция. Обратите внимание, что свойство (isSigner) у PDA здесь 'false' (строка 113), так как клиент не может его подписать. Во время выполнения Программа B подпишет PDA и вызовет инструкцию allocate в программе System Program.

Вы можете найти выделенный PDA в следующей транзакции.

https://explorer.solana.com/tx/5fzdfzbD4281pY1HJm37f1fxSEMGmWtQmbmFPzEiP9v6hWF8GRRZmfcMDvPJugTrs3npnCsaWUGvw15URyBhx3LS?cluster=devnet

3.3 Можно ли использовать Invoke()?

Чтобы показать, что invoke здесь использовать нельзя, мы используем другую программу (т.е. Программу C) для вызова инструкции allocation.

Единственное изменение, которое мы внесли, — замена функции invoke_signed() на invoke(). Мы заметили, что аккаунт создать не удалось.

Ошибка показывает, что для успешного выполнения межпрограммного вызова необходим подписант. Это означает, что требуется функция invoke_signed().

4. Заключение

В этой статье мы рассказали, как реализовать межпрограммный вызов с помощью функции invoke(). Мы также использовали различные примеры, чтобы проиллюстрировать различия между invoke() и invoke_signed(). Оставайтесь с нами, впереди еще больше статей из этой серии.

Читайте другие статьи из этой серии: