10 сентября Waves Platform выпустила новую версию узла, в первой реализации которого есть поддержка смарт-контрактов. В первой части мы сосредоточились на идее смарт-счетов Waves и на том, что отличает их от других существующих решений. В этой части мы сосредоточимся больше на языке смарт-контрактов и инструментах для разработчиков. Эта статья носит технический характер, поэтому, если вы чувствуете проблемы с пониманием, обязательно прочтите Часть 1. Особая благодарность инженеру-исследователю Waves Назиму Фаур за помощь в написании этих статей.

Самая важная часть предыдущей статьи:

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

Контракт Waves для Smart Account всегда должен возвращать True или False , как показано ниже.

О ГАЗе и сборах

Прежде чем перейти к более техническим вещам. Давайте поговорим о газе и сборах. Одна из основных задач, которую мы ставим перед собой, - избавиться от газа для простых операций. Это не значит, что нет никаких комиссий, потому что майнеры также должны быть заинтересованы в выполнении контрактов. Мы решили проблему газа с практической точки зрения. Мы провели тесты производительности и рассчитали скорость выполнения различных операций. Тесты были построены с использованием JMH, результаты доступны здесь. В результате нашего подхода нет газа для не-Turing Complete Smart Account, только фиксированная плата. Кроме того, тесты привели к некоторым ограничениям:

  1. Размер скрипта не может превышать 8 КБ и должен выполняться быстрее, чем 20 операций проверки подписи. Второе правило означает, что проверка для смарт-аккаунта будет не более чем в 20 раз медленнее, чем для обычного аккаунта.
  2. Смарт-аккаунты платят дополнительную комиссию 0,004 WAVES за каждую транзакцию. Минимальная комиссия за транзакцию в сети Waves составляет 0,001 WAVES для обычных аккаунтов и 0,005 для смарт-аккаунтов.

Язык RIDE

Скрипт (контракт) должен быть написан на нашем языке RIDE. RIDE - это не полный по Тьюрингу ленивый, строго типизированный, статически типизированный язык, основанный на выражениях. Эти функции делают его простым, выразительным и свободным от ошибок.

Scala (на нем написана нода Waves) вместе с RIDE под влиянием F #. В документации вы можете найти полное описание стандартной библиотеки (встроенных функций) и типов данных.

Одна из самых интересных функций - сопоставление с образцом, что позволяет очень удобно описывать разные условия для разных типов транзакций:

Если вам интересно, как работает RIDE, вы можете найти более подробную информацию в white paper. Приведены описания всех этапов: парсинг, компиляция, десериализация, расчет стоимости, оценка. Теперь хорошо понимать, что первые два этапа - вне цепочки, десериализация, расчет стоимости и оценка - в цепочке.

Начало работы со смарт-аккаунтами Waves

Приступим к самому интересному и увлекательному - напишем наш первый контракт на смарт-аккаунт. Мы реализуем очень простой пример с учетной записью с несколькими подписями 2 из 3. Мультиподпись - это требование, чтобы транзакции имели две или более подписей, прежде чем они могут быть выполнены.

Если вы начнете писать смарт-контракт волн, вам нужно будет перейти в нашу IDE. Щелкните New Empty Contract , чтобы создать новый файл.

В качестве первого шага давайте определим открытые ключи Алисы, Боба и Купера. Они контролируют счет, и только двое из них могут отправить транзакцию.

В документации есть функция sigVerify , позволяющая проверить подпись:

sigVerify принимает в качестве аргументов тело транзакции, подпись и открытый ключ. Есть объект tx, в котором хранятся данные транзакции. Есть поле tx.bodyBytes с байтами проверяющей транзакции. Также есть поле tx.proofs, которое представляет собой массив с особенностями (до 8).

На втором этапе давайте проверим подписи в правильном порядке:

И последний шаг - проверяем, что у нас больше двух подписей:

Вот и все! Наш контракт с мультиподписью 2 из 3 на RIDE готов. Вы можете получить скомпилированную версию из IDE или развернуть ее прямо в консоли. Последняя версия консольных команд находится в документации, вот пример использования:

Давайте завершим наш пример с несколькими подписями. Полный список контрактов:

let alicePubKey  = base58'B1Yz7fH1bJ2gVDjyJnuyKNTdMFARkKEpV'
let bobPubKey    = base58'7hghYeWtiekfebgAcuCg9ai2NXbRreNzc'
let cooperPubKey = base58'BVqYXrapgJP9atQccdBPAgJPwHDKkh6A8'
let aliceSigned  = if(sigVerify(tx.bodyBytes, tx.proofs[0], alicePubKey  )) then 1 else 0
let bobSigned    = if(sigVerify(tx.bodyBytes, tx.proofs[1], bobPubKey    )) then 1 else 0
let cooperSigned = if(sigVerify(tx.bodyBytes, tx.proofs[2], cooperPubKey )) then 1 else 0
aliceSigned + bobSigned + cooperSigned >= 2

Примечание: оператора возврата нет. Последняя строка контракта - результат.

Для сравнения, одна из распространенных мультисигнатур на Ethereum выглядит так:

pragma solidity ^0.4.22;
contract SimpleMultiSig {
uint public nonce;                 // (only) mutable state
uint public threshold;             // immutable state
mapping (address => bool) isOwner; // immutable state
address[] public ownersArr;        // immutable state
// Note that owners_ must be strictly increasing, in order to prevent duplicates
constructor(uint threshold_, address[] owners_) public {
  require(owners_.length <= 10 && threshold_ <= owners_.length && threshold_ >= 0);
  address lastAdd = address(0);
  for (uint i = 0; i < owners_.length; i++) {
    require(owners_[i] > lastAdd);
    isOwner[owners_[i]] = true;
    lastAdd = owners_[i];
  }
  ownersArr = owners_;
  threshold = threshold_;
}
function execute(uint8[] sigV, bytes32[] sigR, bytes32[] sigS, address destination, uint value, bytes data) public 
{
  require(sigR.length == threshold);
  require(sigR.length == sigS.length && sigR.length == sigV.length);
  bytes32 txHash = keccak256(byte(0x19), byte(0), this, destination, value, data, nonce);
  address lastAdd = address(0); // cannot have address(0) as an owner
  for (uint i = 0; i < threshold; i++) {
    address recovered = ecrecover(txHash, sigV[i], sigR[i], sigS[i]);
    require(recovered > lastAdd && isOwner[recovered]);
    lastAdd = recovered;
}
// If we make it here all signatures are accounted for.
// The address.call() syntax is no longer recommended, see:
// https://github.com/ethereum/solidity/issues/2884
  nonce = nonce + 1;
  bool success = false;
  assembly { success := call(gas, destination, value, add(data, 0x20), mload(data), 0, 0) }
  require(success);
}
function () payable public {}
}

Важность транзакций с данными в смарт-аккаунтах

Транзакции с данными предназначены для хранения произвольных данных в виде пар ключ-значение. Они также связаны с аккаунтом (адресом). Каждое значение имеет связанный с ним тип данных. Поддерживаются четыре типа данных: логический, целочисленный, строковый и байтовый массив. Можно получить данные из транзакций данных в вашем контракте.

Этот небольшой пример показывает, как читать данные из транзакций данных и проверять внешние транзакции в зависимости от значения. Это очень полезно для оракулов и реальных приложений для обработки данных. У этой функции огромный потенциал, но это тема для отдельной статьи.

Дальнейшее обучение

Документация

  1. Полную информацию о смарт-счетах Waves можно найти здесь.
  2. Язык RIDE поддерживает разные типы данных, и все они описаны здесь.
  3. Есть много встроенных функций, таких как addressFromPublicKey и sigVerify, вы можете найти полный список встроенных функций RIDE здесь.

Учебники

  1. Мультиподпись с развертыванием и пением разными аккаунтами (с использованием библиотеки JS)
  2. Вариант использования условного депонирования (с использованием библиотеки Java).

Куда обратиться, если вам понадобится наша помощь?

Сообщество Waves всегда готово помочь вам и ответить на все ваши вопросы в кратчайшие сроки, поэтому не стесняйтесь писать нам сюда:

  1. Раздор
  2. "Форум"
  3. Telegram канал
  4. Github

Что дальше?

Дальнейшее развитие смарт-контрактов Waves - это полный по Тьюрингу язык (или система), который позволит решать все типы задач. Еще одна интересная идея, связанная со смарт-контрактами в экосистеме Waves, - это Smart Assets. Они работают аналогичным образом - полные контракты не по Тьюрингу, которые могут быть прикреплены к токену. Например, с помощью Smart Assets можно будет удерживать токены до определенной высоты блокчейна или запрещать сделки P2P. Подробнее о смарт-активах - в этой статье.

В последний выпуск смарт-ассеты не были включены, но они уже реализованы в кодовой базе Waves. Если у вас есть идеи или варианты использования, пишите их в комментариях, мы будем рады обсудить.