August 16, 2022

Начало работы с инструкциями 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. Наш проект:

aleo new foo

Это создаст директорию 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 - это типизированный язык. На данный момент доступны следующие типы данных:

Определяемые 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. Это вводит некоторые ограничения, которых нет в обычных языках программирования. Например, когда вы используете троичный оператор для создания ветви в потоке программы, вы должны иметь оба результата заранее (т.е. результат каждой ветви должен быть вычислен и известен перед выполнением троичного оператора). Логическое сравнение должно выполняться между регистрами, а не между возвращаемыми значениями вызовов функций.