Перейти к основному содержимому

Solidity vs FunC

Введение

В разработке смарт-контрактов используют языки вроде Solidity для Ethereum и FunC для TON. Solidity — это объектно-ориентированный язык высокого уровня со строгой типизацией, отчасти вдохновлённый C++, Python и JavaScript. Он спроектирован именно для разработки смарт-контрактов, которые выполняются на блокчейн-платформе Ethereum.

FunC также является языком высокого уровня, используемым для программирования смарт-контрактов на блокчейне TON. Он объектно-ориентированный, C-подобный, статически типизированный.

В следующих разделах кратко проанализируем различные аспекты этих языков: типы данных, хранилища, функции, структуры управления потоком, словари (hashmaps).

Различия Solidity и FunC

Схема хранения

Solidity

Solidity обеспечивает плоскую модель хранения: это значит, что все переменные состояния хранятся в едином непрерывном блоке памяти, его называют хранилищем. Там всё хранится в формате «ключ-значение», где каждый ключ является 256-битным целым числом, представляющим собой номер ячейки хранения, а каждое значение — это 256-битное слово, хранящееся в этой ячейке. Ячейки нумеруются последовательно, начиная с нуля, и в каждой из них может храниться только одно слово. Solidity позволяет разработчику сформировать структуру хранилища, используя ключевое слово «storage» для определения переменных состояния. Порядок, в котором задаются переменные, определяет их положение в хранилище.

FunC

Данные постоянного хранилища в блокчейне TON хранятся в виде ячеек. Ячейки играют роль памяти в стековой TVM. Чтобы прочитать данные из ячейки, надо преобразовать её в тип slice, а затем получить биты данных и ссылки на другие ячейки, считывая их из этого «слайса». Для записи данных надо сохранить биты данных и необходимые ссылки на другие ячейки в объект типа builder, а затем создать из этого builder новую ячейку.

Типы данных

Solidity

Solidity включает следующие базовые типы данных:

  • Целые числа: со знаком и беззнаковые (signed/unsigned integers)
  • Булевы значения (boolean)
  • Адреса (addresses) — используются для хранения адресов кошельков Ethereum или смарт-контрактов, обычно около 20 байт. Тип адреса может быть дополнен ключевым словом payable, что отличается его использованием только для адресов кошельков, а также функциями transfer и send.
  • Массивы байтов (byte arrays) — объявляются ключевым словом bytes, представляют собой массив фиксированного размера, используемый для хранения предопределенного количества байтов до 32, обычно задаются вместе с ключевым словом.
  • Литералы (literals) — неизменяемые значения, такие как адреса, рациональные и целые числа, строки, юникод и шестнадцатеричные числа, которые могут храниться в переменной.
  • Перечисления (enums)
  • Массивы (arrays) — фиксированные или динамические
  • Структуры (structs)
  • Маппинги (mappings)

FunC

В случае FunC основными типами данных являются:

  • Целые числа (integers)
  • Ячейка (cell) — базовая для TON непрозрачная структура данных, которая содержит до 1023 бит и до 4 ссылок на другие ячейки
  • Slice и Builder — специальные объекты для чтения и записи в ячейки
  • Continuation — еще одна разновидность ячейки, которая содержит готовый к выполнению байт-код TVM
  • Кортежи (tuple) — упорядоченная коллекция, в которой может быть до 255 компонентов с произвольными типами значений, возможно, различными.
  • Тензоры (tensor) — это упорядоченная коллекция, готовая к массовому присвоению вроде такого: (int, int) a = (2, 4). Частным случаем тензорного типа является (). Он означает, что функция не возвращает никакого значения или не имеет аргументов.

В настоящее время FunC не поддерживает определение пользовательских типов. Можете прочитать больше о типах на странице Statements.

Объявление и использование переменных

Solidity

Solidity — это язык со статической типизацией, то есть тип каждой переменной должен быть указан при её объявлении.

uint test = 1; // Declaring an unsigned variable of integer type
bool isActive = true; // Logical variable
string name = "Alice"; // String variable

FunC

FunC — более абстрактный и функционально-ориентированный язык. Он поддерживает динамическую типизацию и функциональный стиль программирования.

(int x, int y) = (1, 2); // A tuple containing two integer variables
var z = x + y; // Dynamic variable declaration

Прочитать можно на странице Statements.

Циклы

Solidity

Solidity поддерживает циклы for, while и do { ... } while.

Если вы хотите сделать что-то 10 раз, вы можете сделать это следующим образом:

uint x = 1;

for (uint i; i < 10; i++) {
x *= 2;
}

// x = 1024

FunC

FunC, в свою очередь, поддерживает циклы repeat, while и do { ... } until. Цикл for не поддерживается. Если вы хотите выполнить тот же код, что и в примере выше, но на Func, вы можете использовать repeat

int x = 1;
repeat(10) {
x *= 2;
}
;; x = 1024

Узнать больше можно на странице Statements.

Функции

Solidity

Подход Solidity к объявлениям функций сочетает ясность и контроль. В этом языке программирования каждая функция инициируется ключевым словом function, за которым следует имя функции и её параметры. Тело функции заключено в фигурные скобки, явно определяющие область действия. Кроме того, возвращаемые значения указываются с помощью ключевого слова returns.

Основное отличие Solidity заключается в определении области видимости функций. Можно обозначить функцию как public, private, internal или external. Эти определения задают условия, на которых разработчики могут вызывать различные части контракта или внешние сущности. Ниже приведён пример, в котором мы задаём глобальную переменную num на языке Solidity:

function set(uint256 _num) public returns (bool) {
num = _num;
return true;
}

FunC

В свою очередь программа, написанная на FunC, по сути представляет собой список объявлений/определений функций и объявлений глобальных переменных. Объявление функции в FunC обычно начинается с необязательного декларатора, за которым следует возвращаемый тип и имя функции.

Далее перечисляют парамутры, и затем объявление заканчивается модификаторами — такими, как impure, inline/inline_ref и method_id. Эти модификаторы настраивают видимость функции, возможность модифицировать хранилище контракта, а также инлайнинг. Ниже приведён пример на FunC, в котором мы сохраняем переменную как ячейку в постоянном хранилище:

() save_data(int num) impure inline {
set_data(begin_cell()
.store_uint(num, 32)
.end_cell()
);
}

Можете прочитать больше на странице Функции.

Структуры управления потоком выполнения

Solidity

Большинство операторов из известных языков с фигурными скобками доступны и в Solidity, включая: if, else, while, do, for, break, continue, return, с обычной семантикой, известной из C или JavaScript.

FunC

FunC поддерживает классические операторы if-else, а также ifnot, циклы repeat, while и do/until. Также с версии v0.4.0 поддерживаются операторы try-catch.

Можете прочитать больше на странице Statements.

Словари

Словари, они же структура данных hashmap, крайне важны для разработки контрактов Solidity и FunC, так как позволяют разработчикам эффективно хранить и извлекать данные в смарт-контрактах. Особенно данные, связанные с определённым ключом, к примеру, баланс счёта пользователя или факт владения активом.

Solidity

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

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

FunC

Аналогом mapping в FunC являются словари или TON hashmap. В контексте TON «hashmap» — это структура данных, представленная деревом ячеек. Hashmap маппит ключи со значениями произвольного типа, чтобы их можно было быстро находить и изменять. Абстрактное представление hashmap в TVM — это дерево Patricia или компактное двоичное дерево.

Работа с потенциально большими деревьями ячеек может содержать несколько сложностей. Каждая операция обновления создаёт значительное количество ячеек, а каждая построенная ячейка стоит 500 единиц газа. Это означает, что эти операции могут исчерпать имеющиеся ресурсы, если их использовать неосторожно. Чтобы избежать превышения лимита газа, ограничьте количество обновлений словаря за одну транзакцию.

Кроме того, двоичное дерево для N пар ключ-значение содержит N-1 форков, что означает в общей сложности не менее 2N-1 ячеек. Хранилище смарт-контракта ограничено 65536 уникальными ячейками, поэтому максимальное количество записей в словаре составляет 32768 или чуть больше, если есть повторяющиеся ячейки.

Можете прочитать больше на странице Словари в TON.

Взаимодействие смарт-контрактов

Solidity и FunC представляют разные подходы к взаимодействию со смарт-контрактами. Основное различие заключается в механизмах вызова и взаимодействия между контрактами.

Solidity

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

// External contract interface
interface IReceiver {
function receiveData(uint x) external;
}

contract Sender {
function sendData(address receiverAddress, uint x) public {
IReceiver receiver = IReceiver(receiverAddress);
receiver.receiveData(x); // Direct call of the contract function
}
}

FunC

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

Рассмотрим пример, в котором отправитель смарт-контракта должен отправить сообщение с номером, а получатель смарт-контракта должен получить этот номер и выполнить над ним некоторые манипуляции.

Изначально получатель смарт-контракта должен описать, как он будет получать сообщения.

() recv_internal(int my_balance, int msg_value, cell in_msg, slice in_msg_body) impure {
int op = in_msg_body~load_uint(32);

if (op == 1) {
int num = in_msg_body~load_uint(32);
;; do some manipulations
return ();
}

if (op == 2) {
;;...
}
}

Шаги при получении сообщения:

  1. recv_internal() — эта функция выполняется, когда к контракту обращаются напрямую в блокчейне. Например, когда к нашему контракту обращается другой контракт.
  2. Функция принимает сумму баланса контракта, сумму входящего сообщения, ячейку с исходным сообщением и объект in_msg_body типа slice, в котором хранится только тело полученного сообщения.
  3. Наше тело сообщения будет хранить два целых числа. Первое число — это 32-битное беззнаковое целое число op, определяющее операцию, которую нужно выполнить. Можно провести аналогию с Solidity и представить себе op как сигнатуру функции.
  4. Чтобы прочитать из полученного slice op как число, мы используем load_uint().
  5. Далее мы выполняем бизнес-логику конкретной операции. Обратите внимание, что в этом примере мы её опустили.

Далее смарт-контракт отправителя должен корректно отправить сообщение. Для этого используется метод send_raw_message, который ожидает сериализованное сообщение в качестве аргумента.

int num = 10;
cell msg_body_cell = begin_cell().store_uint(1,32).store_uint(num,32).end_cell();

var msg = begin_cell()
.store_uint(0x18, 6)
.store_slice("EQBIhPuWmjT7fP-VomuTWseE8JNWv2q7QYfsVQ1IZwnMk8wL"a) ;; in the example, we hardcode the recipient's address
.store_coins(0)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_ref(msg_body_cell)
.end_cell();

send_raw_message(msg, mode);

Шаги отправки сообщения:

  1. Сначала нам нужно «построить» наше сообщение. Полную структуру отправки можно найти здесь.
  2. Тело сообщения представляет собой ячейку. В msg_body_cell мы делаем следующее: begin_cell() — создаёт Builder для будущей ячейки, в нём сначала используем store_uint — сохраняет первый uint в Builder (1 - это наш op), затем store_uint — сохраняет второй uint в Builder (num — это наш номер, которым мы будем манипулировать в контракте-получателе), end_cell() — создаёт ячейку.
  3. Чтобы прикрепить тело, которое придёт в recv_internal в сообщении, мы ссылаемся на собранную ячейку в самом сообщении с помощью store_ref.
  4. Отправка сообщения.

В этом примере показано, как смарт-контракты могут общаться друг с другом.

Можете прочитать больше на странице Внутренние сообщения.

См. также

Was this article useful?