» » » Обновляемые смарт-контракты в сети Ethereum

 

Обновляемые смарт-контракты в сети Ethereum

Автор: admin от 26-12-2017, 21:55, посмотрело: 108

Мотивация



Контракты сети Ethereum иммутабельны – единожды загруженные в сети (блокчейн), они не могут быть изменены. Специфика бизнеса или разработки могут потребовать обновить код, но при традиционном подходе это становится проблемой.



Популярные причины необходимости обновления




  • Ошибки в коде

  • Изменение бизнес требований

  • Принятие предложений сообщества об изменении работы контракта



Описание технического решения



Реализация требуемого функционала — обновление кода, планируется через разделение кода на составляющие:




  • Данные — смарт-контракты без логики и предоставляющие исключительно пространство для хранения данных;

  • Бизнес-логика — смарт-контракты описывающие логику извлечения данных из хранилища и их изменения;

  • Входные точки — иммутабельные контракты ведут учет обновления бизнес-логики и предоставляют конечному пользователю ссылку на актуальный контракт бизнес-логики

  • Source Url



    pragma solidity ^0.4.18;
    
    contract UIntStorage {
      uint private value;
    
      function setValue(uint _value) external returns (uint) {
        value = _value;
        return value;
      }
    
      function getValue() external view returns (uint) {
        return value;
      }
    }


    Как видно из названия и реализации контракта – хранилище ничего не знает о том как его будут использовать и выполняет задачу инкапсуляции поля uint private value



    Бизнес-логика



    Договоримся, что взаимодействие с нашей бизнес-логикой будет осуществляться через два метода: increaseCounter и getCounter для увеличения счетчика и получения текущего значения соответственно, о чем явно опишем в интерфейсе – ~/contracts/examples/counter/ICounter.sol:



    Source Url



    pragma solidity ^0.4.18;
    
    interface ICounter {
      function increaseCounter() public returns (uint);
      function getCounter() public view returns (uint);
    }


    Далее опишем смарт-контракт бизнес-логики из первой стадии реализующий ICounter интерфейс и использующий ранее описанное хранилище – ~/contracts/examples/counter/IncrementCounter.sol:



    Source Url



    pragma solidity ^0.4.18;
    
    import "./ICounter.sol";
    import "../../base/UIntStorage.sol";
    
    contract IncrementCounter is ICounter {
      UIntStorage public counter;
      function IncrementCounter(address _storage) public {
        counter = UIntStorage(_storage);
      }
      function increaseCounter() public returns (uint) {
        return counter.setValue(getCounter() + 1);
      }
      function getCounter() public view returns (uint) {
        return counter.getValue();
      }
    }


    Важно отметить, что IncrementCounter не имеет внутреннего состояния (не хранит данные), кроме ссылки на хранилище.



    Если договориться передавать в метод increaseCounter и getCounter ссылку на хранилище первым аргуметом, можно реализовать стейт-лесс бизнес-логику



    Вносим изменения в ~/contracts/examples/counter/ICounter.sol:



    Source Url



    pragma solidity ^0.4.18;
    
    interface ICounter {
      function increaseCounter(address _storage) public returns (uint);
      function getCounter(address _storage) public view returns (uint);
      function validateStorage(address _storage) public view returns (bool);
    }


    Теперь методы бизнес-логики ждут первым агрументом ссылку на хранилище, а так же реализуют метод проверки хранилища на валидность: validateStorage(address _storage)



    Внесем изменения в реализацию первой стадии – ~/contracts/examples/counter/IncrementCounter.sol:

    Source Url



    pragma solidity ^0.4.18;
    
    import "./ICounter.sol";
    import "../../base/UIntStorage.sol";
    
    contract IncrementCounter is ICounter {
      modifier validStorage(address _storage) {
        require(validateStorage(_storage));
        _;
      }
    
      function increaseCounter(address _storage) 
        validStorage(_storage) 
        public returns (uint) 
      {
        UIntStorage counter = UIntStorage(_storage);
        require(counter.isUIntStorage());
        return counter.setValue(counter.getValue() + 1);
      }
    
      function getCounter(address _storage) 
        validStorage(_storage) 
        public view returns (uint) 
      {
        UIntStorage counter = UIntStorage(_storage);
        require(counter.isUIntStorage());
        return counter.getValue();
      }
    
      function validateStorage(address _storage) 
        public view returns (bool) 
      {
        return UIntStorage(_storage).isUIntStorage();
      }
    }


    Перед переходом к реализации следующей стадии и обновлению контракта бизнес-логики, напишем пару тестов и убедимся, что бизнес-логика работает как запланировано.



    Тестирование



    Данный репозиторий является проектом фреймворка Truffle и предоставляет удобный функционал для тестирования: truffle test.



    Я не буду подробно описывать процесс написания тестов, но если эта тема вам интересна – напишите мне в телеграм @alerdenisov и я подготовлю статью с best-practice тестирования контрактов.



    ~/test/IncrementCounter.test.js:



    import expectThrow from './utils/expectThrow'
    
    const IncrementCounter = artifacts.require('./IncrementCounter.sol')
    const UIntStorage = artifacts.require('./UIntStorage.sol')
    const BoolStorage = artifacts.require('./BoolStorage.sol')
    
    contract('IncrementCounter', ([owner, user]) => {
      let counter, storage, fakeStorage
      before(async () => {
        storage = await UIntStorage.new()
        fakeStorage = await BoolStorage.new()
        counter = await IncrementCounter.new()
      })
    
      it('Should receive 0 at begin', async () => {
        const currentValue = await counter.getCounter(storage.address)
        assert(currentValue.eq(0), `Uxpected counter value: ${currentValue.toString(10)}`)
      })
    
      it('Should increase value on 1', async () => {
        await counter.increaseCounter(storage.address)
        const newValue = await counter.getCounter(storage.address)
        assert(newValue.eq(1), `Unxpected counter value: ${newValue.toString(10)}`)
      })
    
      it('Should store 1 after increment', async () => {
        const storedValue = await storage.getValue()
        assert(storedValue.eq(1), `Unxpected stored value: ${storedValue.toString(10)}`)
      })
    
      it('Should validate storage', async () => {
        await counter.validateStorage(storage.address)
      })
    
      it('Should unvalidate fake storage', async () => {
        await expectThrow(counter.validateStorage(fakeStorage.address))
      })
    })


    Запуск тестов покажет, что все "прекрасно":



      Contract: IncrementCounter
        ? Should receive 0 at begin
        ? Should increase value on 1 (63ms)
        ? Should store 1 after increment
        ? Should validate storage
        ? Should unvalidate fake storage
    
      5 passing (301 ms)


    Но на самом деле это не так. Допишем промежуточный тест "неавторизированного" взаимодействия с хранилищем:



      it('Should prevent non-authenticated write', async () => {
        await expectThrow(storage.setValue(100))
      })


      Contract: IncrementCounter
        ? Should receive 0 at begin
        ? Should increase value on 1 (58ms)
        1) Should prevent non-authenticated write
        2) Should store 1 after increment
        ? Should validate storage
        ? Should unvalidate fake storage
    
      4 passing (330ms)
      2 failing


    Source Url



    Владение хранилищем



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



    Основное преимущество смарт-контрактов в том, что они гарантируют участникам обмена то, что данные (состояние) не будет изменено никак иначе кроме как декларирует смарт-конракт. Но сейчас изменения ничем не ограничены.



    Задача сделать так, чтобы изменять хранилище мог исключительно актуальный контракт бизнес-логики.



    Для явного ограничения взаимодействия с хранилищем, воспользуемся паттерном Ownable из фреймворка zeppelin-solidity (подробнее c паттерном можно ознакомиться в документации к фреймворку).



    Наследуем хранилище от Ownable контракта и добавим модификатор onlyOwner на метод setValue():



    Source Url



    pragma solidity ^0.4.18;
    
    import "zeppelin-solidity/contracts/ownership/Ownable.sol";
    
    contract UIntStorage is Ownable {
      uint private value;
    
      function setValue(uint _value) onlyOwner external returns (uint) {
        value = _value;
        return value;
      }
    
      function getValue() external view returns (uint) {
        return value;
      }
    
      function isUIntStorage() external pure returns (bool) {
        return true;
      }
    }


    Поздравляю, теперь в наше хранилище может писать только ассоциированный владелец хранилища! Теперь уже 3 из 6 теста проваливаются! Давайте в тестах "в ручную" передадим бизнес-логики управление хранилищем:



    Source Url



      before(async () => {
        storage = await UIntStorage.new()
        fakeStorage = await BoolStorage.new()
        counter = await IncrementCounter.new()
    
        await storage.transferOwnership(counter.address)
      })


    Теперь все тесты проходят, но встает второй вопрос: "Как управлять владением хранилища при обновлении бизнес-логики"



    Общий контроллер



    Перед реализацией общего контроллера сделаем еще один контракт счетчика, но уже второй стадии – ~/contracts/examples/counter/IncrementCounterPhaseTwo.sol:



    Source Url



    pragma solidity ^0.4.18;
    
    import "./IncrementCounter.sol";
    
    contract IncrementCounterPhaseTwo is IncrementCounter {
      function increaseCounter(address _storage) 
        validStorage(_storage) 
        public returns (uint) 
      {
        UIntStorage counter = UIntStorage(_storage);
        return counter.setValue(counter.getValue() + 10);
      }
    }


    Теперь когда у нас есть две реализации счетчика и Ownable хранилище, становится понятно, что необходимо как-то "просить" одну реализацию отдать другой управление хранилищем. Добавим метод transferStorage(address _storage, address _counter) в интерфейс счетчиков – ~/contracts/examples/counter/ICounter.sol:



    Source Url



    pragma solidity ^0.4.18;
    
    interface ICounter {
      function increaseCounter(address _storage) public returns (uint);
    
      function getCounter(address _storage) public view returns (uint);
    
      function validateStorage(address _storage) public view returns (bool);
    
      function transferStorage(address _storage, address _counter) public returns (bool);
    }


    Договоримся, что финальная реализация ICounter должна после вызова метода transferStorage отдавать управление хранилищем адресу переданному в параметр _counter:



    Source Url



      function transferStorage(address _storage, address _counter) validStorage(_storage) public returns (bool) {
        return UIntStorage(_storage).transferOwnership(_counter);
      }


    Давайте допишем тесты передачи прав новой логике и проверим результат increaseCounter метода после смены логики:



      it('Should transfer ownership', async () => {
        await counter.transferStorage(storage.address, secondCounter.address);
      })
    
      it('Should reject increase from outdated counter', async () => {
        await expectThrow(counter.increaseCounter(storage.address));
      })
    
      it('Should increase counter with new logic', async () => {
        await secondCounter.increaseCounter(storage.address)
        const newValue = await secondCounter.getCounter(storage.address)
        assert(newValue.eq(11), `Unxpected counter value: ${newValue.toString(10)}`)
      })


    Выполнение тестов может дать ложное ощущение, что все работает:



      Contract: IncrementCounter
        ? Should receive 0 at begin
        ? Should increase value on 1 (75ms)
        ? Should prevent non-authenticated write
        ? Should store 1 after increment
        ? Should validate storage
        ? Should unvalidate fake storage
        ? Should transfer ownership
        ? Should reject increase from outdated counter
        ? Should increase counter with new logic (47ms)
    
      9 passing (500ms)


    Но спешу вас огорчить, эти изменения опять открыли зеленный свет злоумышленикам:



      it('Should reject non-authenticated transfer storage', async () => {
        await expectThrow(secondCounter.transferStorage(storage.address, user, { from: user }))
      })
    
      it('Should reject increase from user fron previous test', async () => {
        await expectThrow(storage.setValue(100500, { from: user }))
      })
    
      it('Should store 11 as before', async () => {
        const storedValue = await storage.getValue()
        assert(storedValue.eq(11), `Unxpected stored value: ${storedValue.toString(10)}`)
      })


      Contract: IncrementCounter
        ? Should receive 0 at begin (46ms)
        ? Should increase value on 1 (55ms)
        ? Should prevent non-authenticated write
        ? Should store 1 after increment
        ? Should validate storage
        ? Should unvalidate fake storage
        ? Should transfer ownership
        ? Should reject increase from outdated counter
        ? Should increase counter with new logic (46ms)
        1) Should reject non-authenticated transfer storage
        2) Should reject increase from user fron previous test
        3) Should store 11 as before
    
      9 passing (611ms)
      3 failing


    Основная задача общего контроллера будет управлять передачей прав и не допускать кого-угодно к этому процессу. Сначала изменим IncrementCounter по аналогии с UIntStorage, чтобы он тоже наследовал логику Ownable и ограничивал взаимодействие с хранилищем:



    Source Url



    pragma solidity ^0.4.18;
    
    import "./ICounter.sol";
    import "../../base/UIntStorage.sol";
    
    contract IncrementCounter is ICounter, Ownable {
      modifier validStorage(address _storage) {
        require(validateStorage(_storage));
        _;
      }
    
      function increaseCounter(address _storage) 
        onlyOwner validStorage(_storage) 
        public returns (uint) 
      {
        UIntStorage counter = UIntStorage(_storage);
        require(counter.isUIntStorage());
        return counter.setValue(counter.getValue() + 1);
      }
    
      function getCounter(address _storage) 
        validStorage(_storage) 
        public view returns (uint) 
      {
        UIntStorage counter = UIntStorage(_storage);
        require(counter.isUIntStorage());
        return counter.getValue();
      }
    
      function validateStorage(address _storage) 
        public view returns (bool) 
      {
        return UIntStorage(_storage).isUIntStorage();
      }
    
      function transferStorage(address _storage, address _counter)
        onlyOwner validStorage(_storage) 
        public returns (bool) 
      {
        UIntStorage(_storage).transferOwnership(_counter);
        return true;
      }
    }


    Приступим к реализации контроллера. Основные требования к контроллеру:

    1) Учет текущей реализации счетчика

    2) Обновление реализации счетчика

    2) Перемещение прав на хранилище при обновлении реализации

    3) Отклонение попыток неавторизированного обновление реализации



    ~/contracts/examples/counter/CounterContrller.sol:



    Source Url



    pragma solidity ^0.4.18;
    
    import "zeppelin-solidity/contracts/ownership/Ownable.sol";
    import "./ICounter.sol";
    import "../../base/UIntStorage.sol";
    
    contract CounterController is Ownable {
      UIntStorage public store = new UIntStorage();
    
      ICounter public counter;
    
      event CounterUpdate(address previousCounter, address nextCounter);
    
      function updateCounter(address _counter) 
        onlyOwner
        public returns (bool) 
      {
        if (address(counter) != 0x0) {
          counter.transferStorage(store, _counter);
        } else {
          store.transferOwnership(_counter);
        }
    
        CounterUpdate(counter, _counter);
        counter = ICounter(_counter);
      }
    
      function increaseCounter() public returns (uint) {
        return counter.increaseCounter(store);
      }
    
      function getCounter() public view returns (uint) {
        return counter.getCounter(store);
      }
    }


    increaseCounter и getCounter не более, чем просто внешние методы взаимодействия с аналогичными в текущей реализации ICounter. Вся логика контроллера находится в небольшом методе: updateCounter(address _counter).



    Метод updateCounter принимает адресс на реализацию счетчика и перед установкой его как адреса новой реализации счетчика? передает ему права на хранилище (от себя или от предыдущей в зависимости от состояния).



    Помните про третью стадию? Я опущу код ее реализации, тем более, что отличается от второй только одной строчкой. Просто скажу, что в третьей стадии счетчик будет увеличивать значение умножением на самого себя: value = value * value.



    Давайте напишем немного тестов и убедимся, что контроллер работает и выполняет поставленные перед ним задачи:



    import expectThrow from './utils/expectThrow'
    
    const IncrementCounter = artifacts.require('./IncrementCounter.sol')
    const IncrementCounterPhaseTwo = artifacts.require('./IncrementCounterPhaseTwo.sol')
    const MultiplyCounterPhaseThree = artifacts.require('./MultiplyCounterPhaseThree.sol')
    const CounterController = artifacts.require('./CounterController.sol')
    const UIntStorage = artifacts.require('./UIntStorage.sol')
    
    contract('CounterController', ([owner, user]) => {
      let controller, counterOne, counterTwo, counterThree, storage
    
      before(async () => {
        controller = await CounterController.new()
        storage = UIntStorage.at(await controller.store())
        counterOne = await IncrementCounter.new()
        counterTwo = await IncrementCounterPhaseTwo.new()
        counterThree = await MultiplyCounterPhaseThree.new()
    
        await counterOne.transferOwnership(controller.address)
        await counterTwo.transferOwnership(controller.address)
        await counterThree.transferOwnership(controller.address)
      })
    
      it('Shoult create storage', async () => {
        assert(await storage.isUIntStorage(), 'Controller doesn't create proper storage')
      })
    
      it('Should change counter implementation', async () => {
        await controller.updateCounter(counterOne.address)
        assert(await controller.counter() === counterOne.address, `Unxpected counter in controller (${await controller.counter()} but expect ${counterOne.address})`)
      })
    
      it('Should increase counter on 1', async () => {
        await controller.increaseCounter()
        const value = await controller.getCounter()
        assert(value.eq(1), `Unxpected counter value: ${value.toString(10)}`)
      })
    
      it('Should update counter', async () => {
        await controller.updateCounter(counterTwo.address)
        assert(await controller.counter() === counterTwo.address, `Unxpected counter in controller (${await controller.counter()} but expect ${counterTwo.address})`)
      })
    
      it('Should increase counter on 10 after update', async () => {
        await controller.increaseCounter()
        const value = await controller.getCounter()
        assert(value.eq(11), `Unxpected counter value: ${value.toString(10)}`)
      })
    
      it('Should reject non-authenticated update', async () => {
        await expectThrow(controller.updateCounter(counterTwo.address, { from: user }))
      })
    
      it('Should update on phase three and increase counter to 11*11 after execution', async () => {
        await controller.updateCounter(counterThree.address)
        await controller.increaseCounter()
        const value = await controller.getCounter()
        assert(value.eq(121), `Unxpected counter value: ${value.toString(10)}`)
      })
    })
    


      Contract: CounterController
        ? Shoult create storage
        ? Should change counter implementation (53ms)
        ? Should increase counter on 1 (52ms)
        ? Should update counter (55ms)
        ? Should increase counter on 10 after update (56ms)
        ? Should reject non-authenticated update
        ? Should update on phase three and increase counter to 11*11 after execution (89ms)
    
      7 passing (684ms)


    Как видите контроллер свою задачу выполняет, а код нашего счетчика стал обновляемым.



    Резюме



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



    Но у данного подхода есть ряд существенных недостатков и комментариев:




    • Увеличивается стоимость транзакций (объем потребляемого газа), но не значительно. Если есть желающие провести подсчеты – буду признателен или ожидайте в ближайшем будущем от меня.

    • Появляется роль администратора, но решается передачей прав на контроллер смарт-контракту децентрализованного голосования за принятие обносвлений

    • Сложность проектирования, писать код в одном монолитном контексте в разы проще и требует меньше внимания к потокам данных и сообщений. Реализация state-less требует еще большего внимания от разработчика. Решается вызовом реализации через delegatecall. Напишите мне если нужно написать продолжение с передачей состояния через delegatecall.



    UPD:

    @dzentota в телеграм обсуждении отметил недоработку: лишний вызов isUIntStorage() в методах IncrementCounter. (Исправление)[https://github.com/alerdenisov/upgradable-contracts/blob/master/contracts/examples/counter/IncrementCounter.sol]



    Источник: Хабрахабр

    Категория: Операционные системы » Windows

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

    Добавление комментария

    Имя:*
    E-Mail:
    Комментарий:
    Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
    Введите два слова, показанных на изображении: *