Сегодня только ленивый не запускает очередной бесполезный проект на блокчейне, в этом уроке я расскажу как сделать что-то имеющее практическое применение. В качестве примера возьмем реестр пакетов наподобие npm только использующий цифровую подпись, децентрализованное хранилище Swarm и смарт-контракты на основе Ethereum.


Внимание Это ознакомительная версия контракта, требующая аудита.

Мотивация


Реестр кода на блокчейне имеет ряд приемуществ перед обычным, это:


  • подтверждение цифровой подписью.
  • глобальный доступ.
  • токенизация

Рассмотрим каждый из пунктов по отдельности:


Подтверждение цифровой подписью


Это важный момент в построении доверенной среды. Код должен быть подписан разработчиком, которому вы доверяете. В случае с блокчейном это происходит автоматически. Сегодня при желании любой крупный репозиторий может быть атакован, а код в нем подменен, узнать об этом вы вряд ли сможете. Блокчейн репозиторий защищает нас от подмены путем хранения подтвержденной хэш-суммы.


Глобальный доступ


Думаю, тут все понятно распределенное хранилище дает возможность глобального нецензурируемого доступа к данным.


Токенизация


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


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


Технологии


Для внедрения такого решения будем использовать смарт-контракт на блокчейне Ethereum и распределенную файловую систему Swarm. Для тех кто не знает Swarm – это децентрализованное файловое хранилище наподобие ipfs, только от разработчиков Ethereum. Сразу замечу, что запуск Swarm запланирован на 4 квартал этого года и код может немного измениться, но общая концепция останется прежней.


Файлы в swarm хранятся в виде директорий. Каждая директория содержит манифест – список всех файлов в директории. Манифест это по сути JSON с массивом путей к файлам и их хешами. Каждой директории присваивается bzz-адрес, который является вершиной дерева Меркеля для файлов перечисленных в манифесте. Для идентификации файлов используется хеш-сумма защищенная от атаки удинением сообщения, для этого перед взятием хэш-суммы в начало данных добавляется значение длинны в виде 64-битного числа с порядком байт от младшего к старшему.


Алгоритм


  1. Пакет регистрируется (по имени) смарт-контрактом, получаем хеш-имя.
  2. Код загружается в swarm, получаем bzz-адрес.
  3. Регистрируется новая версия пакета в смарт-контракте с полученным 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(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, следите за изменениями, ну и ставьте звезды, буду признателен.


Ссылки



Пометки и замечания


Ответы на вопросы в комментарии пользователя StanislavL:


Зачем нужна возможность добавления версий с номером меньше текущей?

Некоторые версии пакетов имеют LTS поддержку, поэтому ситуация когда в релиз попадают старшая и младшая версия вполне вероятен.


Нет возможности множественного владения.

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


Какой смысл жечь газ (=тратить деньги) в регистрации очередного билда?

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


Есть событие event Transfered(bytes32 indexed package);
но нет OwnershipChanged. Я бы еще добавлял не только пакет, но и адрес кандидата во владельцы.

Событие Transfered и есть OwnershipChanged: он вызывается когда получающая сторона вызвала receive и владелец сменился. Адрес кандидата не попадает в лог, чтобы не тратить дополнительный газ, так как эти данные могут быть получены из транзакции.

Комментарии (6)


  1. DrPass
    19.04.2018 06:12

    Токенизация позволяет компании владеющей пакетом поставить его на баланс, занести в нематериальные активы, продать и так жалее.

    Это не совсем так. Возможно, в некоторых странах токены могут быть каким-то активом, но в частности в России, в Украине и т.д. они имуществом не являются, и никаких юридически значимых фактов не создают. Если какой-либо бухгалтер и попытается поставить на баланс в качестве НМА какой-то токен, его скушают на следующей аудиторской или не дай боже налоговой проверке.


    1. rumkin Автор
      19.04.2018 13:19

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


  1. StanislavL
    19.04.2018 10:44

    // Регистрируем пакет
    packages[name] = Package(name, msg.sender, 0);


    Не вяжется с

    struct Package {
    address owner;
    // Последний мажорная версия
    uint8 latestMajor;
    // Список всех мажорных версия
    mapping(uint8 => Major) majors;
    }


    Имя не фигурирует


    1. rumkin Автор
      19.04.2018 13:10

      Готово.


      1. StanislavL
        19.04.2018 14:25

        Покритикую немного.
        Зачем нужна возможность добавления версий с номером меньше текущей?
        Нет возможности множественного владения.
        Какой смысл жечь газ (=тратить деньги) в регистрации очередного билда?
        Unpublish необратим.
        Есть событие event Transfered(bytes32 indexed package);
        но нет OwnershipChanged. Я бы еще добавлял не только пакет, но и адрес кандидата во владельцы.


        1. rumkin Автор
          19.04.2018 23:56

          Спасибо за вопросы, добавил их как пометки к тексту статьи.


          Unpublish необратим.

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