В данной статье мы рассмотрим устройство смарт-контрактов в сети ethereum, познакомимся с механизмом создания и валидации цифровой подписи, протоколами fungible и non-fungible токенов, а также как устроено взаимодействие с внешними данными
Перед чтением статьи рекомендую (если еще не знакомы) ознакомиться с whitepaper'ом биткоина
История появления и мотивация
С появлением концепции PoW в блокчейне биткоина, стал возможен безопасный нецентрализованный обмен токенами. Каждая транзакция проходит следующие этапы проверки, выполняемые некоторым скриптом:
сумма UTXO неизменна (UTXO — Unspent transaction output другими словами токены)
цифровая подпись отправителя валидна
Для выполнения более сложной логики проверки транзакции нужно менять скрипт, однако платформа биткоина предоставляет неудобный, не тьюринг полный язык для написания таких скриптов
Цель ethereum'a — объединить и усовершенствовать концепции криптовалют, альткоинов и создать on-blockchain протокол, который позволит разработчикам создавать распределенные приложения на основе инфраструктуры, которая предоставляет масштабируемость, тьюринг полноту, простоту разработки и совместимость
Аккаунты
Роль состояний в эфире выполняют аккаунты. Каждый аккаунт имеет 20-ти байтный адрес. Изменение состояний аккаунтов происходит за счет передачи сообщений
Поля аккаунта:
nonce
— счетчик, который служит для того чтобы каждая транзакция была обработана лишь 1 разether balance
— внутренняя валюта, используется для оплаты транзакцийcontract code
(if present)storage
— массив данных, изначально пустой
Типы аккаунтов:
Externally owned accounts
— externally owned accounts не содержат исполняемого кода. Пользователь может отправить сообщение с externally owned account, подписав транзакцию
Contract accounts
— при получении сообщения, contract account активируется и исполняет код скрипта контракта. Исполняемому процессу доступны чтение и запись из внутреннего хранилища, отправка сообщений, создание других контрактов
Сообщения
Сообщения ethereum'a аналог транзакций bitcoin'a с тремя главными отличиями:
Сообщение в эфире может быть создано как externally owned акканутом, так и contract аккаунтом, тогда как транзакция в биткоине может быть создана только externally owned аккаунтом (пользователем)
Сообщения в эфире поддерживают явным образом передачу данных
У получателя сообщения (если это contract account) есть возможность вернуть ответ
Структура сообщения
Nonce
GASPRICE
GASLIMIT
Recipent
Ether value
Data
подпись отправителя: r,s,v (ECDSA)
Nonce:
Nonce
- порядковый номер транзакций, отправленных с данного адреса. При каждой отправке транзакции значение Nonce
увеличивается на единицу. Кроме того, Nonce предотвращает повторную атаку: злоумышленник может захотеть исполнить подписанную транзакцию еще раз, однако при валидации Nonce
транзакции и текущий Nonce аккаунта будут не совпадать и транзакция будет считаться не валидной
GASPRICE and GASLIMIT:
Для того чтобы предотвратить зацикливание нод при обработки сообщения, каждое сообщение должно указывать за какое максимальное число "шагов" GASLIMIT
выполнения кода отправитель готов платить. GASPRICE
— сколько отправитель платит за выполнение одного шага
Если при выполнении сообщения заканчивается GAS
, то все участники возвращаются в исходное состояние и уже потраченный GAS
передается майнеру в качестве комиссии.
(Однако после форка EIP-1559 теперь майнеры получают фиксированное вознаграждение за каждый сформированный блок)
Recipient:
В поле recipient хранится 20-ти байтный адрес получателя. Получателем может быть как externally owned аккаунт, так и contract аккаунт
Ether value:
В поле ether value хранится число, которое показывает сколько эфира отправитель передает получателю с учетом комисси за транзакцию (GAS
и комиссия за пересылку данных)
Data:
Это поле используется для передачи данных смарт контракту, также возможны вызовы функций внутри смарт контракта, если сообщение содержит одну из инструкций CALL, DELEGATECALL
Если поле data пустое, это означает что сообщение предназначено только для передачи ehter'a, а не для исполнения кода
ECDSA (Elliptic Curve Digital Signature Algorithm):
Более подробно ознакомиться с тем как работает ECDSA можно тут
Параметры эллиптической кривой, при помощи которой происходит шифрование, определены стандартом secp256k1
Кобминация {r,s,v} генерируется из SHA1 хеша сообщения и private ключа отправителя{r,s,v}
передается как 65-ти байтная строка:
32 байта r
32 байта s
1 байт v
Валидация сообщений
Этапы проверки сообщений на нодах:
Проверить корректность кол-ва и содержания полей сообщения (Nonce транзакции совпадает с Nonce аккаунта, сообщение имеет корректную подпись)
STARTGAS*GASPRICE
— комиссия которую платит отправитель. Если у отправителя достаточно ether'a → снять сумму комиссии с его аккаунта и увеличить Nonce аккаунта на 1, иначе вернуть ошибкуGAS=STARTGAS*GASPRICE
— счетчик того сколько осталось газа для выполнения транзакции, при каждом выполнении команды или передачи байт данных счетчик уменьшаетсяПередать
Ether value
получателю, если аккаунта получателя не существует, то создать его. Если получатель — contract account, то исполнить его код до конца или до окончанияGAS
'aЕсли передача данных упала из-за недостаточного кол-ва
ether
'a на аккаунте или из-за окончанияGAS
'a, то вернуть состояния получателя и отправителя в исходное состояние и добавить комиссию к кошельку аккаунта майнера (актуально до форка EIP-1559)Вернуть весь оставшийся
GAS
отправителю, и перечислить комиссию майнеру
Contract account code execution environment
Код смарт контракта исполняется на стековой витруальной машине. У операторов байткода Ethereum Virtual Machine (скоращенно EVM) есть доступ к трем типам структур данных:
stack выполнения
memory — динамический массив, который можно расширять бесконечно
long-term contract storage — key/value "холодное" хранилище. Холодное потому что доступно даже после выполнения кода контракта
Высокоуровневые языки программирования смарт-контрактов: Solidity и Vyper
Solidity — объектно-ориентированный c++ like язык:
статическая типизация
наследование
модули
user-defined types
Пример смарт-контракта вендинг машины на solidity:
--**pragma solidity 0.8.7;
contract VendingMachine {**
// Declare state variables of the contract
address public owner;
mapping (address => uint) public cupcakeBalances;
// When 'VendingMachine' contract is deployed:
// 1. set the deploying address as the owner of the contract
// 2. set the deployed smart contract's cupcake balance to 100
constructor() {
owner = msg.sender;
cupcakeBalances[address(this)] = 100;
}
// Allow the owner to increase the smart contract's cupcake balance
function refill(uint amount) public {
require(msg.sender == owner, "Only the owner can refill.");
cupcakeBalances[address(this)] += amount;
}
// Allow anyone to purchase cupcakes
function purchase(uint amount) public payable {
require(msg.value >= amount * 1 ether, "You must pay at least 1 ETH per cupcake");
require(cupcakeBalances[address(this)] >= amount, "Not enough cupcakes in stock to complete this purchase");
cupcakeBalances[address(this)] -= amount;
cupcakeBalances[msg.sender] += amount;
}
}
msg
— объект сообщения которое вызвало исполнение/создание контракта
На примере видно, что в конструкторе VendingMachine в поле owner записывается адрес отправителя сообщения. То есть в будущем увеличивать cupcakeBalances
сможет только аккаунт, который задеплоил смарт-контракт
Внешние источники данных
Для адекватной работы алгоритма консенсуса нужно чтобы каждая нода получала одинаковый результат при исполнении сообщения, при обращении внутри смарт-контракта к внешнему API ноды априори не будут получать одинаковый результат, если данные меняются во времени
Как тогда работать с внешними данными? Решение — oracle. Oracle реализует некий middle end между off-chain данными и on-chain смарт-контрактами. Он оформляет доступ к данным в отдельную транзакцию и записывает его на блокчейн. Для этого oracle обычно состоит из смарт-контракта и некоторых off-chain скриптов, которые состоят из двух основных действий:
вытащить данные из какого-то API
отправить сообщение с данными нодам
Затем смарт-контракт обращается к данным, которые лежат в сообщении и таким образом являются частью блокчейна
Важно чтобы oracle был тоже децентрализованным, иначе теряется смысл всего блокчейна
Система токенов
fungible токены
on-blockchain токены имеют много приложений, например они могут имитировать фиатные деньги или другие активы, могут служить "документом" на владение каким-либо объектом. Система токенов — это база данных с одной операцией: вычесть X единиц с аккаунта А и прибавить X единиц аккаунту B, при условии что:
Баланс А ≥ X
Есть подпись подтверждающая согласие аккаунта А
Наиболее распространенный стандарт реализации системы fungible токенов — ERC20, который определяется двумя сегментами:
6 функций:
TotalSupply
— общее количество токеновBalanceOf
— возвращает баланс кошелька по заданному адресуTransfer
— позволяет владельцу контракта отправить токены заданному адресуTransferFrom
— отправляет токены с одного адреса на другой, главное отличие отTransfer
: контракт может отправлять токены владельца автоматическиApprove
— проверяет возможность передачи токенов с аккаунта владельца контракта заданному адресуAllowance
— возвращет остаток на кошельке по заданному адресу
2 события:
Transfer
— это событие вызывается во время любого перевода токенов с одного кошелька на другой и дает подробную информацию о нихApprove
— это событие вызывается каждый раз в функцииapprove
Non-fungible token
Невзаимозаменяемые токены — вид криптографических токенов, каждый экземпляр которых уникален и не может быть заменен другим токеном. Cтандарт для реализации NFT ERC-721 является аналогом ERC-20 для fungible токенов
Источники: