Сегодня только ленивый не запускает очередной бесполезный проект на блокчейне, в этом уроке я расскажу как сделать что-то имеющее практическое применение. В качестве примера возьмем реестр пакетов наподобие npm только использующий цифровую подпись, децентрализованное хранилище Swarm и смарт-контракты на основе Ethereum.
Внимание Это ознакомительная версия контракта, требующая аудита.
Мотивация
Реестр кода на блокчейне имеет ряд приемуществ перед обычным, это:
- подтверждение цифровой подписью.
- глобальный доступ.
- токенизация
Рассмотрим каждый из пунктов по отдельности:
Подтверждение цифровой подписью
Это важный момент в построении доверенной среды. Код должен быть подписан разработчиком, которому вы доверяете. В случае с блокчейном это происходит автоматически. Сегодня при желании любой крупный репозиторий может быть атакован, а код в нем подменен, узнать об этом вы вряд ли сможете. Блокчейн репозиторий защищает нас от подмены путем хранения подтвержденной хэш-суммы.
Глобальный доступ
Думаю, тут все понятно распределенное хранилище дает возможность глобального нецензурируемого доступа к данным.
Токенизация
Управлять и владеть пакетом может один или несколько участников или даже организация. Сегодня все учетные записи и данные в большинстве случаев регистрируются на сотрудников, как следствие при увольнении передача прав на такие ресурсы может стать проблемой.
Токенизация позволяет компании владеющей пакетом поставить его на баланс, занести в нематериальные активы, продать и так жалее. Так же при продаже самой компании токенизированные активы будет легко учесть и включить в стоимость.
Технологии
Для внедрения такого решения будем использовать смарт-контракт на блокчейне Ethereum и распределенную файловую систему Swarm. Для тех кто не знает Swarm – это децентрализованное файловое хранилище наподобие ipfs, только от разработчиков Ethereum. Сразу замечу, что запуск Swarm запланирован на 4 квартал этого года и код может немного измениться, но общая концепция останется прежней.
Файлы в swarm хранятся в виде директорий. Каждая директория содержит манифест – список всех файлов в директории. Манифест это по сути JSON с массивом путей к файлам и их хешами. Каждой директории присваивается bzz-адрес, который является вершиной дерева Меркеля для файлов перечисленных в манифесте. Для идентификации файлов используется хеш-сумма защищенная от атаки удинением сообщения, для этого перед взятием хэш-суммы в начало данных добавляется значение длинны в виде 64-битного числа с порядком байт от младшего к старшему.
Алгоритм
- Пакет регистрируется (по имени) смарт-контрактом, получаем хеш-имя.
- Код загружается в swarm, получаем bzz-адрес.
- Регистрируется новая версия пакета в смарт-контракте с полученным bzz-адресом.
Реализация
Модель данных
Хранение версии ведется по принципам semver без именованных веток (alpha
, rc0
и т.п. не поддерживаются). Все пакеты хранятся в списке, где ключ — имя пакета, а значение – дерево версий. Выглядит это так:
// Пакет
struct Package {
address owner;
// Последний мажорная версия
uint8 latestMajor;
// Список всех мажорных версия
mapping(uint8 => Major) majors;
}
// Мажорная версия пакета, содержит все минорные.
struct Major {
// Последняя минорная версия
uint8 latestMinor;
// Список минорных версий
mapping(uint8 => Minor) minors;
}
// Минорная версия пакета, содержит все билды.
struct Minor {
// Последний минорный билд
uint16 latestBuild;
// Список билдов.
mapping(uint16 => Build) builds;
}
// Собственно номер билда и указание bzz-адреса.
struct Build {
// bzz-адрес
bytes32 bzz;
// Флаг
bool isPublished;
}
Память контракта
Для работы контракта используются три списка:
// Список пакетов.
mapping(bytes32 => Package) packages;
// Сопоставление имен и хешей для внешних инструментов.
mapping(bytes32 => bytes) names;
// Адреса получателей трансферов.
mapping(bytes32 => address) transfers;
Регистрация пакета
Регистрация пакета нужна для того, чтобы связать название пакета и владельца, а так же конвертировать имя в хеш. Хеширование имени нужно для того, чтобы работа с пакетом, имеющим название любой длинны всегда занимала одинаковое время/потребляла одинаковое количество газа:
function register(bytes _name)
public
returns(bytes32)
{
// Убеждаемся, что имя имеет ненулевую длинну
require(_name.length > 0);
// Конвертируем имя в хеш
bytes32 name = resolve(_name);
// Проверяем, что пакет не имеет владельца
require(packages[name].owner == address(0));
// Регистрируем пакет
packages[name] = Package(name, msg.sender, 0);
// Заносим имя в список для обратного разрешения имен
names[name] = _name;
return name;
}
Вызов из JS:
const Web3 = require('web3');
const {abi} = require('./contract.js');
const web3 = new Web3(new Web3('https://rinkeby.infura.io/'));
// Initialize contract instance
const reg = new web3.eth.Contract(abi, '0x57147069B117fD911Da6c43F3fBdC54a7A7D8C1d');
reg.methods.register('hello_world').send()
.then((hash) => console.log(hash));
Загрузка в Swarm
Данные в Sqarm загружаются по протоколу HTTP, при этом для загрузки директории можно поместить файлы в tar-архив:
tar -c * | curl -H "Content-Type: application/x-tar" --data-binary @- http://localhost:8500/bzz:/
В результате получим 32-битную вершину дерева Меркеля (bzz-адрес), например:
1e0e21894d731271e50ea2cecf60801fdc8d0b23ae33b9e808e5789346e3355e
Для получения файлов из swarm необходимо получить список файлов:
curl -s http://localhost:8500/bzz-list:/ccef599d1a13bed9989e424011aed2c023fce25917864cd7de38a761567410b8/ | jq .
> {
"common_prefixes": [
"dir1/",
"dir2/",
"dir3/"
],
"entries" : [
{
"path": "file.txt",
"contentType": "text/plain",
"size": 9,
"mod_time": "2017-03-12T15:19:55.112597383Z",
"hash": "94f78a45c7897957809544aa6d68aa7ad35df695713895953b885aca274bd955"
}
]
}
Для экспериментов можете использовать сайт https://swarm-gateways.org/.
Регистрация новой версии
Для регистрации новой версии достаточно вызвать метод register с указанием хеша имени, номеров версии (мажорный, минорный, билд) и bzz-адреса.
function publish(bytes32 _package, uint8 _major, uint8 _minor, uint16 _build, bytes32 _bzz)
public
{
Package storage package = packages[_package];
// Проверка парва владения.
require(package.owner == msg.sender);
// Проверка версии на уникальность
require(hasBuild(_package, _major, _minor, _build) == false);
// Объявляем необходимые структуры
package.majors[_major].minors[_minor].builds[_build] = Build(_bzz, true);
Major memory major = package.majors[_major];
Minor memory minor = package.majors[_major].minors[_minor];
// Обновление значения последней мажорной версии
if (package.latestMajor < _major) {
package.latestMajor = _major;
}
// Обновление значения последней минорной версии для мажроной версии.
if (major.latestMinor < _minor) {
package.majors[_major].latestMinor = _minor;
}
// Обновление значения последннего билда минорной версии.
if (minor.latestBuild < _build) {
package.majors[_major].minors[_minor].latestBuild = _build;
}
emit Published(_package, _major, _minor, _build);
}
reg.methods.publish(hash, 0, 1, 0, bzz).send();
Передача прав
Как уже говорилось выше право владения пакетом можно передавать, методы реализующие этот функционал: transfer
и receive
. Метод transfer
назначает нового владельца, после чего новый владелец может получить пакет вызвав метод receive
.
До вызова
receive
управление пакетом сохраняется за текущим владельцем. Это защищает от ошибочных действий, а так же требует подтверждения принимающей стороной, что позволяет сделать сделку по передаче двусторонней: никто не сможет навязать вам владение без вашего согласия.
Методы контракта
- register — регистрирует контракт.
- resolve — конвертирует имя в хеш.
- publish — регистрация новой версии. Генерирует событие
Published
. - unpublish — отзыв версии. Генерирует событие
Unpublished
. - getOwner — возвращает текущего владельца пакета.
- hasBuild — возвращает статус последнего опубликованного билда.
- isPublished — возвращает статус версии.
- getLatestMajorVersion — получение номера последнего мажроной версии пакета.
- getLatestMinorVersion — получение номера последнего минорной версии пакета для указанной мажорной версии.
- getLatestBuildVersion — получение номера последнего билда пакета для указанной минорной версии.
- transfer — передача пакета новому владельцу.
- receive — получение пакета новым владельцем.
Ограничения
Данная схема исключает возможность отзыва мажорных или минорных версии. Отозвать возможно только определенный билд. Так же нельзя отзывать последний билд минорной версии. Для этого предварительно необходимо опубликовать новый, а затем отозвать предыдущий билд. Это исключает необходимость перебирать дерево версий для переназначения новой latest версии и таким образом сокращает количество потребляемого газа.
Заключение
В таком виде все версии пакетов становятся глобально идентифицируемы по хешу, что позволяет, для примера, привязывать номера CVE к определенным версиям кода. А за счет применения блокчейна и распределенной структуры, изменения в вашем коде гарантированно достигнут пользователей.
Если есть идеи, как улучшить данный код, оставляйте issues или PR, следите за изменениями, ну и ставьте звезды, буду признателен.
Ссылки
- Репозиторий на github.
- Photo by Maarten van den Heuvel on Unsplash
Автор: rumkin