Здесь есть интересный компромисс: больше логики в смарт-контрактах можно использовать для реализации большего количества функций безопасности: тайм-аутов, лимитов расходов, мультиподписи, хранилищ и т. Д. Однако чем больше логики в смарт-контракте, тем больше поверхность атаки и тем больше вероятность появления ошибок, которые могут подорвать функции безопасности.

Мы можем рассматривать спектр инструментов управления активами от самых простых до более сложных. Самый простой способ защитить ваш эфир - использовать один закрытый ключ, соответствующий адресу Ethereum, иногда называемому «Учетная запись конечного пользователя». С этим методом вообще не нужно беспокоиться о логике смарт-контрактов, поэтому мы устранили этот риск. Однако использование всего лишь одного ключа означает, что у вас есть единственная точка отказа.

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

Простой контракт с несколькими подписями

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

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

В итоге мы получили контракт, который выталкивает большую часть логики за пределы цепочки, где каждый владелец мульти-подписи отвечает за создание подписи, которая разрешает транзакции, а затем используется одна функция для представления всех подписей к контракту для проверки. Мы используем предложенную спецификацию ERC191, которая является попыткой стандартизации формата подписей. Одно целое число nonce используется для предотвращения атак повторного воспроизведения.

Что касается пользовательского интерфейса, идея состоит в том, что у каждого держателя ключей с несколькими подписями будет пользовательский интерфейс, в котором они будут вводить данные транзакции, которую они хотят отправить (в идеале, на автономном компьютере). Затем на онлайн-машине «оператор» собирал все подписи от держателя ключей и отправлял фактическую транзакцию, содержащую все подписи. Оператору не нужно будет иметь какой-либо фактический контроль над средствами, держатели ключей с несколькими подписями в конечном итоге являются теми, кто имеет право выполнять транзакции.

Полный код представлен здесь:

pragma solidity 0.4.14;
contract SimpleMultiSig {

  uint public nonce;                // (only) mutable state
  uint public threshold;            // immutable state
  mapping (address => bool) isOwner; // immutable state
  address[] public ownersArr;        // immutable state

  function SimpleMultiSig(uint threshold_, address[] owners_) {
    if (owners_.length > 10 || threshold_ > owners_.length || threshold_ == 0) {throw;}

    for (uint i=0; i<owners_.length; i++) {
      isOwner[owners_[i]] = true;
    }
    ownersArr = owners_;
    threshold = threshold_;
  }

  // Note that address recovered from signatures must be strictly increasing
  function execute(uint8[] sigV, bytes32[] sigR, bytes32[] sigS, address destination, uint value, bytes data) {
    if (sigR.length != threshold) {throw;}
    if (sigR.length != sigS.length || sigR.length != sigV.length) {throw;}

    // Follows ERC191 signature scheme: https://github.com/ethereum/EIPs/issues/191
    bytes32 txHash = sha3(byte(0x19), byte(0), this, destination, value, data, nonce);

    address lastAdd = address(0); // cannot have address(0) as an owner
    for (uint i = 0; i < threshold; i++) {
        address recovered = ecrecover(txHash, sigV[i], sigR[i], sigS[i]);
        if (recovered <= lastAdd || !isOwner[recovered]) throw;
        lastAdd = recovered;
    }

    // If we make it here all signatures are accounted for
    nonce = nonce + 1;
    if (!destination.call.value(value)(data)) {throw;}
  }

  function () payable {}
}

и код также доступен в следующем репозитории github.

Преимущества

Некоторые полезные свойства этого контракта:

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

Отсутствие сложных переходов между состояниями делает невозможным завершение контракта в замороженном состоянии с недоступными средствами, как это описано здесь Эмином Гюн Сирером. Поскольку единственно возможный переход между состояниями - это простой увеличивающийся счетчик, контракт всегда будет в правильном состоянии. Маловероятно, что счетчик переполнится, поскольку используется 32-байтовое целое число. Кроме того, тестирование упрощается тем, что тестируется только одна функция (помимо конструктора).

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

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

Будущая работа: формальная проверка

Простота контракта может сделать его хорошим кандидатом для создания формальной спецификации и выполнения формальной проверки с использованием этой спецификации - математического доказательства того, что код соответствует спецификации. Для этого может потребоваться переписать контракт на языке, на котором компиляция в EVM также была формально проверена. Формальная проверка EVM в последнее время привлекает все большее внимание с недавним выпуском формальной семантики для EVM.

Следующим шагом может быть написание простого контракта с несколькими подписями в LLL или даже в чистом байт-коде EVM, чтобы ограничить риски со стороны компилятора Solidity.

Резюме

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

Благодарим Нейта Раша за предложения по улучшению контракта.