Содержание статьи
Писать умные контракты для Ethereum — не такая сложная задача, как может показаться. Чтобы рассказать тебе, как это делать, мы решили провести небольшой эксперимент. В этой статье описана разработка умного контракта, который разыгрывает трехмесячную подписку на «Хакер». Установив клиент и запустив контракт, ты сможешь принять участие!
- Сегодня в эфире. Краткий экскурс в Ethereum
- Интервью: Виталик Бутерин о себе и о создании Ethereum
- Внимание, это розыгрыш! Пишем простой смарт-контракт
- Ставим Ethereum на Raspberry Pi
- Распределенное автономное ограбление. Как хакер разорил автоматическую корпорацию DAO
Начать лучше всего именно с запуска готового контракта — так будет намного проще понять, что к чему. Код ты можешь скачать с сервера Х и заглянуть внутрь. Он состоит из двух частей: одна — интерфейс на JSON, вторая — собственно код контракта на языке Solidity.
А теперь разберем наш контракт по частям.
Погружение
Вот самое начало:
contract HackerLottery {
struct Participant {
string nickname;
string email;
}
Participant[] public participantsProfile;
mapping(address => uint) participantsIDs;
address public owner;
string public lotteryName;
string public lotteryDescription;
uint public registeredTo;
int public winnerID;
}
int
и uint
— это типы для целых чисел со знаком и без знака. Они могут иметь размер от 8 до 256 бит с шагом 8 бит (uint32 — целое число без знака длиной 32 бита). Собственно, uint
и int
— псевдонимы для uint256 и int256 соответственно. В таких переменных мы храним время, до которого продлится розыгрыш (unix time), а также идентификатор победителя.
address
— тип длиной 20 байт (160 бит), который предназначен для работы с адресами. Мы храним адрес создателя контракта, чтобы только он имел возможность вызывать некоторые функции.
string
— массив, динамично изменяемая строка в юникоде. В lotteryName
и lotteryDescription
мы храним название и описание лотереи.
mapping
— словарь, содержит пары ключ — значение. В participatnsIDs
мы храним адреса и уникальные идентификаторы участников для того, чтобы с указанного аккаунта можно было зарегистрироваться только один раз.
struct
— с помощью структур мы можем создавать свои кастомные переменные, в данном случае мы храним в массиве participantsProfiles
никнейм и почту каждого участника.
public
— позволяет читать значение внешнему контракту или клиенту. Если указать private
, то доступ к переменной будет только внутри контракта либо у унаследованного контракта.
Теперь описываем модификаторы (modifier
) функций. Они служат для проверки входных данных.
modifier noEther() {
if (msg.value > 0) throw;
_
}
modifier onlyOwner() {
if (msg.sender != owner) throw;
_
}
Знак подчеркивания обычно включается в конце тела функции, чтобы показать, где будет выполняться основной код. Тут у нас два модификатора: один прерывает выполнение, если в транзакции был совершен перевод эфира, другой проверяет права доступа к некоторым функциям (они есть только у создателя контракта).
Функции
Сначала идет функция-конструктор. Она носит то же имя, что и сам контракт, и вызывается при его создании. В параметрах мы передаем число, которое указывает длительность лотереи, и сохраняем временную метку конца лотереи.
function HackerLottery(string _lotteryName, string _lotteryDescription, uint _duration) {
lotteryName = _lotteryName;
lotteryDescription = _lotteryDescription;
registeredTo = now + 1 days * _duration;
owner = msg.sender;
winnerID = -1;
}
О функциях важно знать еще вот что: существует некая транзакция, которая меняет состояние переменных и сохраняет это состояние. В Ethereum хранится вся история изменений.
Функция регистрации сохраняет в массиве participantsProfiles
данные нового участника, а также связанный с адресом ID аккаунта. Обрати внимание, что мы добавили ранее созданный модификатор noEther
. А еще мы здесь впервые встречаемся с сообщениями.
function registration(string _nickname, string _email) noEther returns (bool success) {
if (participantsIDs[msg.sender] < 0) return false;
participantsIDs[msg.sender] = participantsProfile.length;
participantsProfile[participantsProfile.length++]=Participant({nickname: _nickname, email: _email});
registered(_nickname, _email, msg.sender);
return true;
}
event registered(string _nickname, string _email, address _addr);
Event
— это сообщения, на которые, зная адрес и название ивента, может подписаться любой сторонний слушатель. В данном случае мы сообщаем о том, что зарегистрировался новый пользователь.
Далее определяем функции для изменения имени и описания лотереи (changeName
и changeDescription
) — сделать это может только создатель, а потому добавим модификатор onlyOwner
. Кроме того, создатель контракта может изменить время окончания лотереи (extendDuration
) и передать право на совершение этих действий другому аккаунту (transferOwnership
).
function changeName(string _name) onlyOwner {
lotteryName = _name;
}
function changeDescription(string _description) onlyOwner {
lotteryDescription = _description;
}
function extendDuration(uint _daysToAdd) onlyOwner {
registeredTo = now + 1 days * _daysToAdd;
extendedDuration(registeredTo);
}
function transferOwnership(address _newOwner) onlyOwner {
owner = _newOwner;
ownershipTrasfered(owner);
}
Функция playLottery
содержит сам процесс розыгрыша. Внутри мы проверяем, пришло ли время розыгрыша и была ли уже разыграна лотерея. Как устроен генератор случайных чисел, я рассказывать не буду — догадайся сам.
function playLottery() onlyOwner returns (int id) {
if (now < registeredTo || winnerID != -1) throw;
uint length = participantsProfile.length;
uint winner = (uint(sha3(block.blockhash(block.number-1), now))%length);
winnerID = int(winner);
winnerFounded(winnerID);
return winnerID;
}
Функция killContract
с помощью вызова suicide(owner)
(owner
— адрес создателя) переводит все средства на счету контракта на счет создателя, а также удаляет контракт из блокчейна. После этого с ним нельзя будет выполнять какие-либо операции.
function killContract() onlyOwner {
suicide(owner);
}
event extendedDuration(uint _registeredTo);
event winnerFounded(int _id);
event ownershipTrasfered(address _owner);
Далее добавим «константную» функцию getNumberOfParticipants()
. Слово constant
говорит о том, что эта функция не изменяет значения переменных и может вызываться локально. То есть ее выполнение не стоит нисколько газа.
function getNumberOfParticipants() constant returns (uint length) {
return participantsProfile.length;
}
Заключение
Вот мы и закончили наш небольшой контракт, с помощью которого мы разыграем подписку на журнал. Он должен помочь тебе в освоении Ethereum и продемонстрировать его возможности. А в идеале — вдохновить на великие свершения.
Где еще почитать о разработке контрактов
Лучший старт для изучения разработки смарт-контрактов и платформы Ethereum — это серия из небольших туториалов на ethereum.org и документация по языку разработки Solidity.
Еще тебе могут пригодиться материалы на DappsForBeginners и статья разработчиков из компании ConsenSys A 101 Noob Intro to Programming Smart Contracts on Ethereum.