Начало работы с инструкциями Aleo
Текст оригинальной статьи: https://www.entropy1729.com/getting-started-aleo-instructions/
Мой Дискорд: useless_dorozhkina#1394
Данный гайд является пошаговым разбором нескольких легких примеров, чтобы помочь вам с инструкциями Aleo.
Инструкции - это нечто вроде assembly для программ Aleo. Программирование более высокого уровня может быть осуществлено с помощью Leo, с которым вы можете ознакомиться здесь.
Прежде, чем мы начнем, убедитесь, что у вас установлен и запущен Rust. Вы можете найти несколько примеров в репозитории проекта Aleo.
Установите Aleo
Вы можете установить aleo
, создав из исходного кода, как показано ниже (мы рекомендуем устанавливать Aleo этим путем):
# Установите исходный код git clone https://github.com/AleoHQ/aleo && cd aleo # Установите Aleo $ cargo install --path .
- Сборка из crates.io
(Эта опция устарела, мы рекомендуем собирать из исходного кода, чтобы получить последнюю версию)
В качестве альтернативы, вы можете установить компилятор Aleo из репозитория crates.io. В вашем терминале введите:
cargo install aleo
На этом шаге вы можете ввести команду new
. Наш проект:
aleo new foo
Первые шаги
Создание нового проекта Aleo
Чтобы создать новый проект, мы используем команду new
. Наш проект:
Это создаст директорию foo и файлы с базовой структурой проекта:
- README.md со скелетом README с инструкциями о том, как компилировать.
- main.aleo - главный файл исходного кода.
- program.json, содержащий в себе идентификатор проекта в формате JSON. В частности, адрес разработчика и его приватный ключ для программы.
Давайте откроем main.aleo и зададим функцию сложения:
// The 'foo.aleo' program. program foo.aleo; function sum: input r0 as u32.public; input r1 as u32.private; add r0 r1 into r2; output r2 as u32.private;
Мы рассмотрим, что значит данный код через секунду. Для начала, мы создадим нашу программу foo.
Компилирование проекта Aleo
Чтобы скомпилировать проект, в главной директории введите:
aleo build
⏳ Compiling 'foo.aleo'... • Loaded universal setup (in 1478 ms) • Built 'sum' (in 6323 ms) ✅ Built 'foo.aleo' (in "[...]/foo")
Сначала "универсальная настрйка" загружается в вашу среду. Вы можете прочитать больше об этом здесь или в докладе о Marlin.
Как только завершена универсальная установка, все функции в вашем файле main.aleo будут созданы, сгенерировав в выходной папке это:
- sum.prover - доказывающий для функции суммирования.
- sum.verifier - проверяющий для функции суммирования.
- main.avm - байт-код вашей программы aleo, который будет запущен VM.
Как вы уже можете догадаться, у нас есть только один файл .avm для всей программы, но доказывающий и проверяющий для каждой функции.
Запуск программы
Вы можете запустить всю программу с помощью команды aleo run
с последующим именем функции, которую вы хотите исполнить и ее входными данными. Давайте запустим нашу функцию сложения:
aleo run sum 2u32 3u32
когда выполнение будет завершено, вы увидите подобный вывод:
🚀 Executing 'foo.aleo/sum'... • Calling 'foo.aleo/sum'... • Executed 'sum' (in 1170 ms) ➡️ Output • 5u32 ✅ Executed 'foo.aleo/sum' (in "[...]/foo")
Как вы можете увидеть, исполнение функции сложения длилось 1170 мс, и выходному реестру присвоилось значение 5u32
, представляющее сумму входных данных.
Обзор программы
Давайте исследуем программу foo внутри файла main.aleo:
// Программа 'foo.aleo'. program foo.aleo; function sum: input r0 as u32.public; input r1 as u32.private; add r0 r1 into r2; output r2 as u32.private;
Для начала нам нужно объявить программу, как показано ниже:
program foo.aleo;
Затем мы начинаем писать ее функции (или другие структуры Aleo, такие, как интерфейсы, записи, замыкания, как мы увидим позже). В случае функций все очень просто:
function [function_name]:
Функции состоят из трех основных частей:
- Раздел ввода. Здесь мы объявляем его входные параметры:
input r0 as u32.public; input r1 as u32.private;
Все в инструкциях Aleo объявляется/хранится внутри реестра с типом (i8
,field
,bool
и так далее) и опцией видимости (public
or private
). Реестры называются, как r0
, r1
, ..., rn
. Позже мы более подробно рассмотрим реестры, типы и видимость.
Итак, в данном случае мы используем r0
и r1
, чтобы хранить входные данные, переданные последовательно в программу как значения u32
, где мы можем хранить 32-разрядные целые числа без знака для выполнения нашей операции сложения.
- Раздел инструкций. Следующий раздел содержится в ядре нашей функции. Здесь мы вызываем количество инструкций Aleo, которое нам нужно для того, чтобы наша программа выполняла то, что мы хотим. Например, выполнение операции сложения:
add r0 r1 into r2;
За каждой инструкцией Aleo следуют входные параметры с их определенными типами, а результат хранится в реестре into. Вы можете увидеть больше инструкций Aleo здесь.
- Раздел вывода. Также, как и разделе ввода, раздел вывода делает то же самое для вывода программы. Это возвращаемое значение функции.
output r2 as u32.private;
Углубление в некоторые концепции
Типы
Aleo - это типизированный язык. На данный момент доступны следующие типы данных:
Boolean Field Group I8 I16 I32 I64 I128 U8 U16 U32 U64 U128 Scalar
Interface Record
Реестры
Реестры - это места, в которых вы храните ваши данные, чтобы затем иметь возможность изменять их.
Интерфейсы
Интерфейсы - это структуры данных, определяемых пользователем. Они очень похожи на традиционные структуры в обычных языках программирования. Вы можете хранить Интерфейсы в реестрах, как и в случае с любыми другими типами данных Aleo.
Например, давайте создадим интерфейс, представляющий массив фиксированного размера из 3 элементов. Добавьте его в нижнюю часть файла main.aleo:
interface array3: a0 as u32; a1 as u32; a2 as u32;
Теперь, просто для примера, давайте создадим функцию, которая добавляет единицу к каждому элементу реестра с хранящимся в нем типом данных array3.
function sum_one_to_array3: input r0 as array3.private; add r0.a0 1u32 into r1; add r0.a1 1u32 into r2; add r0.a2 1u32 into r3; cast r1 r2 r3 into r4 as array3; output r4 as array3.private;
Как вы видите, мы можем ввести интерфейс в реестр r0
и получить доступ к элементам с синтаксисом .
. Мы выполняем инструкцию add
для каждого элемента, сохраняя результат в реестры r1
, r2
и r3
, и, наконец, мы используем команду cast для создания нового интерфейса array3 в r4
.
Теперь, давайте запустим программу. В данном случае, единственная новая вещь, которую вам нужно знать, это то, что интерфейсы передаются в cli следующим образом:
"{a0: 1u32, a1: 2u32, a2: 3u32}"
Теперь мы можем выполнить команду aleo run
. Мы очистим проект, чтобы подобрать новый код:
aleo clean && aleo run sum_one_to_array3 "{a0: 0u32, a1: 1u32, a2: 2u32}"
И мы получим новые элемента array3 как выходные данные:
🚀 Executing 'foo.aleo/sum_one_to_array3'... • Calling 'foo.aleo/sum_one_to_array3'... • Executed 'sum_one_to_array3' (in 1331 ms) ➡️ Output • { a0: 1u32, a1: 2u32, a2: 3u32 } ✅ Executed 'foo.aleo/sum_one_to_array3' (in "[...]/foo")
Записи
Запись - это фундаментальная структура данных для шифрования активов пользователя и состояния приложения. Они очень похожи на интерфейсы, но у них есть два обязательных параметра.
record token: owner as address.private gates as u64.private
owner
относится к владельцу адреса Aleo, которому принадлежит запись. gates
- это количество кредитов, которые записи придется потратить.
Записи важны, потому что они представляют базовую структуру Aleo для обработки состояния в вашем приложении.
При выполнении функции Aleo в качестве входных реестров могут передаваться только реестры, принадлежащие адресу приложения. В противном случае будет выдана ошибка, и приложение не будет запущено.
Вы можете найти адрес своего разрабатыемого приложения внутри файла program.json:
{ "program": "foo.aleo", "version": "0.0.0", "description": "", "development": { "private_key": "APrivateKey1zkpFsQNXJwdvjKs9bRsM91KcwJW1gW4CDtF3FJbgVBAvPds", "address": "aleo1x5nz5u4j50w482t5xtqc3jdwly9s8saaxlgjz0wvmuzmxv2l5q9qmypx09" }, "license": "MIT" }
Состояние Алео в двух словах
В Aleo управление состоянием приложения осуществляется с помощью записей. Аккаунт Aleo может создать транзакцию для использования записи и создания на ее месте новой. Записи на Aleo шифруются по адресу владельца записи, это гарантирует, что все записи в Aleo являются полностью приватными.
Ваша первая программа Aleo: Осуществление перевода
// Программа 'foo.aleo'. program foo.aleo; record token: owner as address.private; gates as u64.private; amount as u64.private; function transfer_amount: // запись токена отправителя input r0 as token.record; // адрес получателя input r1 as address.private; // сумма для перевода input r2 as u64.private; // конечный баланс отправителя sub r0.amount r2 into r3; // конечный баланс получателя add 0u64 r2 into r4; // запись токена отправителя после перевода cast r0.owner r0.gates r3 into r5 as token.record; // запись токена получателя после перевода cast r1 0u64 r4 into r6 as token.record; // новая запись токена отправителя output r5 as token.record; // новая запись токена получателя output r6 as token.record;
Сначала мы определили наш собственный тип данных, token
, с двумя обязательными параметрами, owner
и gates
и определяемым пользователем параметром amount
, который показывает количество наших токенов.
Функция transfer_amount
принимает 3 входных значения (sender
, receiver
и amount
) и сохраняет их в 3 реестра (r0
, r1
и r2)
. После этого она вычисляет окончательные балансы для обоих, отправителя и получателя, и сохраняет их в r3
и r4
(используя инструкции sub и add для вычитания и сложения). С помощью этих окончательных значений, она создает выходные записи для отправителя и получателя, сохраняя их в r5
and r6
. Наконец, обе записи отправляются из функции с помощью инструкции output.
Для запуска этой функции первым параметром является входная запись программы. Формат этого параметра такой же, как и для типов интерфейсов:
{ owner: aleo1x5nz5u4j50w482t5xtqc3jdwly9s8saaxlgjz0wvmuzmxv2l5q9qmypx09.private, gates: 0u64.private, amount: 50u64.private }
- owner: публичный адрес программы, который указан в
development.address
файла build/program.json. - gates: gates, которые имеются у записи.
- другие параметры: зависят от самой программы (в данном примере мы использовали параметр amount со значением 50).
Давайте запустим функцию transfer_amount (если вы следуете этим шагам, помните что в поле owner нужно использовать адрес из program.json):
aleo clean && aleo run transfer_amount "{ owner: aleo1x5nz5u4j50w482t5xtqc3jdwly9s8saaxlgjz0wvmuzmxv2l5q9qmypx09.private, gates: 0u64.private, amount: 50u64.private }" aleo1h3gu7fky36y8r7v2x9phc434fgf20g8qd7c7u45v269jfw6vmugqjegcvp 10u64
Мы получим следующие выходные записи:
🚀 Executing 'foo.aleo/transfer_amount'... • Calling 'foo.aleo/transfer_amount'... • Executed 'transfer_amount' (in 3520 ms) ➡️ Outputs • { owner: aleo1x5nz5u4j50w482t5xtqc3jdwly9s8saaxlgjz0wvmuzmxv2l5q9qmypx09.private, gates: 0u64.private, amount: 40u64.private _nonce: 2293253577170800572742339369209137467208538700597121244293392265726446806023group.public } • { owner: aleo1h3gu7fky36y8r7v2x9phc434fgf20g8qd7c7u45v269jfw6vmugqjegcvp.private, gates: 0u64.private, amount: 10u64.private _nonce: 2323253577170856894742339369235137467208538700597121244293392765726742543235group.public } ✅ Executed 'foo.aleo/transfer_amount' (in "[...]/foo")
И все! Вы перевели свои первые собственные токены в Aleo!
Заметьте: _nonce не записан в инструкциях Aleo. Компилятор выводит _nonce в выходных данных записи. Пользователю нужно предоставить его в качестве входной переменной при использовании записи.
Заключительные замечания
Программы Aleo могут компилировать и выполнять криптографические схемы, написанные в инструкциях Aleo. Это вводит некоторые ограничения, которых нет в обычных языках программирования. Например, когда вы используете троичный оператор для создания ветви в потоке программы, вы должны иметь оба результата заранее (т.е. результат каждой ветви должен быть вычислен и известен перед выполнением троичного оператора). Логическое сравнение должно выполняться между регистрами, а не между возвращаемыми значениями вызовов функций.