Эта статья является продолжением цикла о написании умных контрактов на платформе Ethereum. В первой части я пообещал показать, как создать новую криптовалюту на Solidity (в мире блокчейна это является чем-то вроде аналога "Hello, world!"). Но на самом деле в этом нет смысла, так как об этом уже написано несколько хороших статей (пример из доков Solidity, пример с главной страницы Ethereum).
Так что я немного подумал и нашел еще один use case для умных контрактов. В данной статье я покажу, как теоретически автор трояна-шифровальщика может монетизировать свое детище, продавая ключи для расшифровки с помощью умных контрактов.
BTW все написанное ниже имеет чисто образовательный характер.
Общая идея
Шифровальщики появились не вчера и имеют более-менее схожую схему работы. И как правило в этой схеме присутствуют шаги вида
- Оплата выкупа через *coin
- Отправка некоторого ID зараженного ПК + ID транзакции преступникам
- Получение ключа для расшифровки файлов
Вот эти три фрагмента системы мы и попытаемся перенести в блокчейн.
Общая структура проекта
Наш проект будет состоять из двух частей — модуль администрирования и модуль "магазина". Админку мы сделаем в виде отдельного контракта, а контракт магазина просто от него унаследуем. Вообще говоря, в Ethereum можно взаимодействовать между двумя различными контрактами в блокчейне, достаточно лишь знать их адреса и названия интересующих нас функций, но это я продемонстрирую как-нибудь в следующий раз.
Tool box
Писать будем на Solidity версии 0.4.2 (текущая версия на 26 октября 2016). В качестве среды разработки можно использовать онлайн компилятор или только что вышедшую онлайн платформу Ethereum studio. Последняя сделана на базе c9.io, но с фичами для разработки под Ethereum. Сам не пользовался, так как вышла только-только, но выглядит симпатично, хотя документацию ее создатели прячут, наверное, специально.
В качестве клиента-кошелька возьмем Mist, а так как мы делаем просто PoC, то и запускать все контракты будем на своем private блокчейне (как это сделать я рассказывал здесь). Так будет проще, дешевле (в смысле бесплатно) и быстрее.
Пишем код
Для начала создадим модуль администрирования. В него мы добавим функционал для добавления и удаления администраторов, вывода денег и "убийства контракта". Первым делом определим все необходимые переменные и функцию-конструктор. Она должна называться так же как и сам контракт и вызывается лишь однажды (автоматически) — при загрузке контракта в блокчейн.
pragma solidity ^0.4.2; // Указываем версию языка - любая, начиная с 0.4.2 до 0.5 не включительно
contract admin {
// VARIABLES
struct user {
address addr;
string name; // '$uPeR_p0wner_1999'
string desc; // 'CEO & CTO'
}
user owner;
mapping (address => user) adminInfo;
mapping (address => bool) isAdmin;
function admin (string _name, string _desc) {
owner = user({
addr : msg.sender, // msg - дефолтная переменная с информацией о пользователе
name : _name, // вызвавшем контракт. msg.sender - его адрес
desc : _desc // msg.value - сумма в wei, переданная контракту и т.д.
});
isAdmin[msg.sender] = true;
adminInfo[msg.sender] = owner;
}
}
Сам по себе код прост и понятен, благо синтаксис напоминает C++, JS, C, etc. На всякий случай напомню, что оператор struct позволяет создавать кастомные типы данных из уже имеющихся . Mapping, как можно догадаться реализует ассоциативный массив (dict в Python, map в C++).
Здесь мы создали переменную owner, в которой с помощью struct храним адрес, имя и какой-нибудь description для создателя контракта. Контракты в Ethereum имеют так называемый state, то есть в дальнейшем, когда кто-то вызовет контракт, мы сможем воспользоваться этой переменной.
Далее добавим функции, отвечающие за добавление / удаление администратора, вывод денег и уничтожение контракта. Здесь все вообще тривиально, кроме одной штуки — оператора event. Это очень симпатичный, с точки зрения UI и юзабилити вообще, оператор, который позволяет реализовать что-то вроде push уведомлений внутри контракта. Чуть нижу будет скриншот, из которого понятно, как это выглядит на практике.
// EVENTS
event adminAdded(address _address, string _name, string _desc);
event adminRemoved(address _address, string _name, string _desc);
event moneySend(address _address, uint _amount);
// FUNCTIONS
function addAdmin (address _address, string _name, string _desc) {
if (owner.addr != msg.sender || isAdmin[_address]) throw; // Только владелец может добавлять / удалять админов
isAdmin[_address] = true;
adminInfo[_address] = user({addr : _address, name : _name, desc : _desc});
adminAdded(
_address,
_name,
_desc
); // Call event
}
function removeAdmin (address _address) {
if (owner.addr != msg.sender || !isAdmin[_address]) throw;
isAdmin[_address] = false;
adminRemoved(
_address,
adminInfo[_address].name,
adminInfo[_address].desc
); // Call event
delete adminInfo[_address];
}
function getMoneyOut(address _receiver, uint _amount) {
if (owner.addr != msg.sender || _amount <= 0 || this.balance < _amount) throw;
// Функцию может вызвать только владелец, требуемая сумма должна быть положительна
// Последняя проверка - баланс контракта должен быть больше требуемой суммы
if (_receiver.send(_amount)) moneySend(_receiver, _amount); // В случае успеха - вызвать event
}
function killContract () {
if (owner.addr != msg.sender) throw;
selfdestruct(owner.addr); // Все средства на счету контракта будут переведены на адрес владельца
}
Весь этот код просто добавим внутри contract admin {...} после уже написанного и наш модуль для администрирования готов.
Заливаем в блокчейн и наслаждаемся результатом
Этот шаг довольно подробно описан в первой части, не буду на нем останавливаться. Приложу лишь несколько скриншотов работы с уже готовым контрактом. Вот так например в Mist выглядит вызов функции добавления админа:
А вот так выглядят обещанные event-ы:
Магазин
Сначала суть: мы просто сделаем очередь из уже оплаченных заявок на получение ключа. В нашем случае администраторы будут разгребать эту кучу руками (можно автоматизировать, но опять же — как нибудь в следующий раз) и добавлять для каждой заявки ключ в импровизированную БД (сделаем map вида _id > _key). ID, для простоты, у нас будет натуральным числом, а ключом будет string (например ссылка на pastebin).
Сам код поместился в 85 строк, вот он:
contract shop is admin {
// VARIABLES
uint[] orders; // Очередь из оплаченных заказов
uint currentOrder = 0; // Номер последнего необработанного заказа
mapping (uint => string) keys; // Пары ID - ключ
// EVENTS
event keyAdded(uint _ID, string _name, string _desc);
event keyBought(address _address, uint _ID);
// FUNCTIONS
function buyKey(uint _ID) payable { // Без модификатора payable на функцию нельзя отправлять эфир
if (msg.value < 15000000000000000000) throw; // Проверяем, что пользователь отправил нам минимум 15 этеров
orders.push(_ID); // Добавляем его в массив оплаченных заказов
keyBought(
msg.sender,
_ID
);
}
function getKeyByID(uint _ID) returns (string) { // Таким специфическим образом указывается, что вернет функция
return keys[_ID]; // Если ключ для этого ID еще не добавлен, то вернется пустая строка
}
function getLastOrder() returns (uint) {
if (!isAdmin[msg.sender]) throw;
return orders[currentOrder]; // Возвращаем первый ID
currentOrder += 1;
}
function addKey(uint _ID, string _key) {
if (!isAdmin[msg.sender]) throw; // Только администратор может добавить ключ для какого-то ID
keys[_ID] = _key;
keyAdded(
_ID,
adminInfo[msg.sender].name,
adminInfo[msg.sender].desc
);
}
}
Итог
Еще раз подчеркну, что написанное здесь — это прототип с кучей погрешностей и недоделок. Простой пример — в нашем случае массив заявок никак не чистится и только набирает в размере. Из-за этого, когда-нибудь стоимость вызова функции buyKey вырастет до стоимости самого ключа, что как-то неправильно.
Другой, более сложный момент — для того, чтобы хранить порядковый номер последнего обработанного заказа, мы используем переменную currentOrder. А теперь представим ситуацию — есть два админа: Вася в Пекине и Петя в Нью-Йорке. В один момент времени они обратились к функции getLastOrder и оба получили какой-то номер — пусть 23412. Далее каждый из них вызвал функцию addKey и добавил в "базу" ключ для этого заказа, а вместе с ним сохранился его name и desc. В результате, когда майнеры начинают выполнять эти действия, те что поближе к Пекину, быстрее выполнят Васин запрос и state будет иметь один вид, а те что поближе к Нью-Йорку — Петин и state получится другой. В результате какой-то merge conflict.
В любом случае, я надеюсь что мне удалось продемонстрировать, какие фантастические возможности предлагает нам технология блокчейна. Даже этот простой контракт предоставит хакерам возможность монетизировать зловред на порядок проще и безопаснее, по сравнению с привычными схемами.
В следующей статье скорее всего напишу, как прикручивать к контрактам интерфейсы отличные от Mist (например взаимодействие через обычный сайт) ну или как работать с Ethereum в связке с каким-нибудь языком программирования, например Python. Но если есть какие-то предложения — обязательно пишите в комментарии.