Отложенные транзакции для Solidity

Опубликовано 2018-02-09

В этой заметке я хочу поделиться одной интересной техникой при разработке умных контрактов сети Ethereum.

Цифровая подпись

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

contract Checker is owned {
    function byOwner(bytes32 _message, uint8 _v, bytes32 _r, bytes32 _s) view returns (bool) {
        return ecrecover(_message, _v, _r, _s) == owner;
    }
}

В примере, метод byOwner возвращает true тогда и только тогда, когда сообщение подписано приватным ключом владельца контракта. Для подписи сообщения популярные клиенты сети Ethereum предоставляют метод eth.sign(account, message).

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

bytes constant MSGPREFIX = "\x19Ethereum Signed Message:\n32";
address signer = ecrecover(keccak256(MSGPREFIX, _message), _v, _r, _s);

Здесь 32 это длина сообщения в байтах.

Отложенное исполнение

Иногда бывает очень удобно не поселять транзакцию непосредственно в момент создания, а дождаться какого-нибудь события, либо чтобы вовсе поселение транзакции выполнил кто-то другой. Например, Алиса хочет перевести Бобу 2 ether. Здесь возможно несколько вариантов событий:

  • Алиса знает аккаунт Боба:
    • Алиса просто отправляет транзакцию о переводе средств на аккаунт Боба
  • Алиса не знает аккаунт Боба:
    • Алиса создает новый адрес, переводит на него ether и отдает приватный ключ Бобу (так называемый бумажный кошелек)
    • Алиса подписывает отложенную транзакцию о переводе средств

Последний вариант наиболее интересен, Алисе не нужно знать ни адрес Боба ни даже отправлять транзакцию в сеть. Боб сможет сам указать адрес для получения и обеспечить исполнение транзакции.

Как это работает

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

contract DelayedTransfer is owned {
    function DelayedTransfer() payable {}
    function transfer(address _to, uint256 _value, uint256 _nonce, uint8 _v, bytes32 _r, bytes32 _s) {

Это умный контракт, который может принимать эфир на баланс от Алисы. Алиса пополняет его один раз при создании и может раздавать с него эфир пока баланс не обнулится. И один метод для Боба с аргументами:

  • адрес назначения
  • величина перевода
  • соль
  • цифровая подпись

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

mapping(uint256 => bool) public isUsed;

function transfer(address _to, uint256 _value, uint256 _nonce, uint8 _v, bytes32 _r, bytes32 _s) {
  bytes32 message = keccak256(_value, _nonce);
  require(ecrecover(message, _v, _r, _s) == owner && !isUsed[_nonce]);
  isUsed[_nonce] = true;
  _to.transfer(_value);
}

Здесь обязательным условием исполнения транзакции является то, что подписать значение и соль должен владелец контракта, т.е. Алиса. Также проверяется повторное исполнение перевода, переменная isUsed. При этом адрес получателя не фиксируется. Это значит, что цифровая подпись Алисы становится для Боба аналогом купона на перевод и должна хранится в секрете.

Заключение

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