Solidity by Example

Voting

次のコントラクトは割と複雑ですが、Solidityのたくさんの機能を表しています。そのコントラクトは投票を実行しています。もちろん、電子投票のメインの問題はどうやって投票権を正しい人に渡すのかということと、どうやって不正操作を防ぐかです。ここでは全ての問題を解決はしませんが、投票がどの様に行われ、そして 自動かつ公正な 投票数のカウントの方法をお見せします。

そのアイデアとは投票ごとにコントラクトを作り、オプションごとに短い名前をつけるものです。そしてコントラクトの作成者は管理者として各アドレスに投票の権利を付与します。

そのアドレスを持っている人は自分で投票するか、投票の権利を信頼している人に譲渡することもできます。

投票の最後に、winningProposal() は現在の最高投票数を獲得している人を表示します。

pragma solidity >=0.4.22 <0.6.0;

/// @title Voting with delegation.
contract Ballot {
    // This declares a new complex type which will
    // be used for variables later.
    // It will represent a single voter.
    struct Voter {
        uint weight; // weight is accumulated by delegation
        bool voted;  // if true, that person already voted
        address delegate; // person delegated to
        uint vote;   // index of the voted proposal
    }

    // This is a type for a single proposal.
    struct Proposal {
        bytes32 name;   // short name (up to 32 bytes)
        uint voteCount; // number of accumulated votes
    }

    address public chairperson;

    // This declares a state variable that
    // stores a `Voter` struct for each possible address.
    mapping(address => Voter) public voters;

    // A dynamically-sized array of `Proposal` structs.
    Proposal[] public proposals;

    /// Create a new ballot to choose one of `proposalNames`.
    constructor(bytes32[] memory proposalNames) public {
        chairperson = msg.sender;
        voters[chairperson].weight = 1;

        // For each of the provided proposal names,
        // create a new proposal object and add it
        // to the end of the array.
        for (uint i = 0; i < proposalNames.length; i++) {
            // `Proposal({...})` creates a temporary
            // Proposal object and `proposals.push(...)`
            // appends it to the end of `proposals`.
            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0
            }));
        }
    }

    // Give `voter` the right to vote on this ballot.
    // May only be called by `chairperson`.
    function giveRightToVote(address voter) public {
        // If the first argument of `require` evaluates
        // to `false`, execution terminates and all
        // changes to the state and to Ether balances
        // are reverted.
        // This used to consume all gas in old EVM versions, but
        // not anymore.
        // It is often a good idea to use `require` to check if
        // functions are called correctly.
        // As a second argument, you can also provide an
        // explanation about what went wrong.
        require(
            msg.sender == chairperson,
            "Only chairperson can give right to vote."
        );
        require(
            !voters[voter].voted,
            "The voter already voted."
        );
        require(voters[voter].weight == 0);
        voters[voter].weight = 1;
    }

    /// Delegate your vote to the voter `to`.
    function delegate(address to) public {
        // assigns reference
        Voter storage sender = voters[msg.sender];
        require(!sender.voted, "You already voted.");

        require(to != msg.sender, "Self-delegation is disallowed.");

        // Forward the delegation as long as
        // `to` also delegated.
        // In general, such loops are very dangerous,
        // because if they run too long, they might
        // need more gas than is available in a block.
        // In this case, the delegation will not be executed,
        // but in other situations, such loops might
        // cause a contract to get "stuck" completely.
        while (voters[to].delegate != address(0)) {
            to = voters[to].delegate;

            // We found a loop in the delegation, not allowed.
            require(to != msg.sender, "Found loop in delegation.");
        }

        // Since `sender` is a reference, this
        // modifies `voters[msg.sender].voted`
        sender.voted = true;
        sender.delegate = to;
        Voter storage delegate_ = voters[to];
        if (delegate_.voted) {
            // If the delegate already voted,
            // directly add to the number of votes
            proposals[delegate_.vote].voteCount += sender.weight;
        } else {
            // If the delegate did not vote yet,
            // add to her weight.
            delegate_.weight += sender.weight;
        }
    }

    /// Give your vote (including votes delegated to you)
    /// to proposal `proposals[proposal].name`.
    function vote(uint proposal) public {
        Voter storage sender = voters[msg.sender];
        require(sender.weight != 0, "Has no right to vote");
        require(!sender.voted, "Already voted.");
        sender.voted = true;
        sender.vote = proposal;

        // If `proposal` is out of the range of the array,
        // this will throw automatically and revert all
        // changes.
        proposals[proposal].voteCount += sender.weight;
    }

    /// @dev Computes the winning proposal taking all
    /// previous votes into account.
    function winningProposal() public view
            returns (uint winningProposal_)
    {
        uint winningVoteCount = 0;
        for (uint p = 0; p < proposals.length; p++) {
            if (proposals[p].voteCount > winningVoteCount) {
                winningVoteCount = proposals[p].voteCount;
                winningProposal_ = p;
            }
        }
    }

    // Calls winningProposal() function to get the index
    // of the winner contained in the proposals array and then
    // returns the name of the winner
    function winnerName() public view
            returns (bytes32 winnerName_)
    {
        winnerName_ = proposals[winningProposal()].name;
    }
}

Blind Auction

このセクションではEthereum上で完全なブラインドオークションを作成するのがいかに簡単かお見せします。

Simple Open Auction

全員が期間内に入札できるというのが次の単純なオークションのコントラクトの大まかな考え方です。オークション参加者が入札を取り消すことが無いようにするために入札はお金/etherを既に含んでいます。もし最高額が更新されたらその前の最高額を入札していた人に返金します。入札の金額の受領者がお金を受け取るために、コントラクトは入札期間終了後にマニュアルで呼び出されなければいけません。コントラクトは自動でこれを行えません。

pragma solidity >=0.4.22 <0.6.0;

contract SimpleAuction {
    // Parameters of the auction. Times are either
    // absolute unix timestamps (seconds since 1970-01-01)
    // or time periods in seconds.
    address payable public beneficiary;
    uint public auctionEndTime;

    // Current state of the auction.
    address public highestBidder;
    uint public highestBid;

    // Allowed withdrawals of previous bids
    mapping(address => uint) pendingReturns;

    // Set to true at the end, disallows any change.
    // By default initialized to `false`.
    bool ended;

    // Events that will be emitted on changes.
    event HighestBidIncreased(address bidder, uint amount);
    event AuctionEnded(address winner, uint amount);

    // The following is a so-called natspec comment,
    // recognizable by the three slashes.
    // It will be shown when the user is asked to
    // confirm a transaction.

    /// Create a simple auction with `_biddingTime`
    /// seconds bidding time on behalf of the
    /// beneficiary address `_beneficiary`.
    constructor(
        uint _biddingTime,
        address payable _beneficiary
    ) public {
        beneficiary = _beneficiary;
        auctionEndTime = now + _biddingTime;
    }

    /// Bid on the auction with the value sent
    /// together with this transaction.
    /// The value will only be refunded if the
    /// auction is not won.
    function bid() public payable {
        // No arguments are necessary, all
        // information is already part of
        // the transaction. The keyword payable
        // is required for the function to
        // be able to receive Ether.

        // Revert the call if the bidding
        // period is over.
        require(
            now <= auctionEndTime,
            "Auction already ended."
        );

        // If the bid is not higher, send the
        // money back.
        require(
            msg.value > highestBid,
            "There already is a higher bid."
        );

        if (highestBid != 0) {
            // Sending back the money by simply using
            // highestBidder.send(highestBid) is a security risk
            // because it could execute an untrusted contract.
            // It is always safer to let the recipients
            // withdraw their money themselves.
            pendingReturns[highestBidder] += highestBid;
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
        emit HighestBidIncreased(msg.sender, msg.value);
    }

    /// Withdraw a bid that was overbid.
    function withdraw() public returns (bool) {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {
            // It is important to set this to zero because the recipient
            // can call this function again as part of the receiving call
            // before `send` returns.
            pendingReturns[msg.sender] = 0;

            if (!msg.sender.send(amount)) {
                // No need to call throw here, just reset the amount owing
                pendingReturns[msg.sender] = amount;
                return false;
            }
        }
        return true;
    }

    /// End the auction and send the highest bid
    /// to the beneficiary.
    function auctionEnd() public {
        // It is a good guideline to structure functions that interact
        // with other contracts (i.e. they call functions or send Ether)
        // into three phases:
        // 1. checking conditions
        // 2. performing actions (potentially changing conditions)
        // 3. interacting with other contracts
        // If these phases are mixed up, the other contract could call
        // back into the current contract and modify the state or cause
        // effects (ether payout) to be performed multiple times.
        // If functions called internally include interaction with external
        // contracts, they also have to be considered interaction with
        // external contracts.

        // 1. Conditions
        require(now >= auctionEndTime, "Auction not yet ended.");
        require(!ended, "auctionEnd has already been called.");

        // 2. Effects
        ended = true;
        emit AuctionEnded(highestBidder, highestBid);

        // 3. Interaction
        beneficiary.transfer(highestBid);
    }
}

Blind Auction

前のオープンオークションを次はブラインドオークションに拡張します。ブラインドオークションのメリットは入札期限間際の時間に対するプレッシャーが無いことです。ブラインドオークションを誰からも見れるプラットフォームで行うというのは変な感じがしますが、暗号学が助けてくれます。

入札期間 に入札者は実際に入札を行いません。しかし、ハッシュ化されたものだけ送ります。現在、(十分に長い)二つのハッシュ値が同じ値を見つけるのは実用上不可能なので、入札者はその入札値を変更することはできません。 入札期間の終了後入札者は入札値を公開する必要があります。暗号化されていない値を送り、コントラクトはその値をハッシュ化したものが入札期間に送られたハッシュ値と同じか確認します。

もう一つの問題はどうやってオークションに 強制力を持たせ、かつブラインド にするかです。落札した入札者が送金しないことを防ぐ唯一の方法は入札時にお金を支払わせることです。Ethereumでは送金はオープンなので誰でも金額を見ることができます。

次のコントラクトではこの問題を最高額の入札以上のどんな値でも受け入れることで解決しています。もちろんこれは値が公開された時にしか行えないので、いくつかの入札は無効の可能性があり、かつそれは故意的な可能性もあります(高額送金を伴った無効な入札をするために明らかなフラグを立てることもあります)。入札者は高いもしくは低い入札で他の入札者を混乱させることができます。

pragma solidity >0.4.23 <0.6.0;

contract BlindAuction {
    struct Bid {
        bytes32 blindedBid;
        uint deposit;
    }

    address payable public beneficiary;
    uint public biddingEnd;
    uint public revealEnd;
    bool public ended;

    mapping(address => Bid[]) public bids;

    address public highestBidder;
    uint public highestBid;

    // Allowed withdrawals of previous bids
    mapping(address => uint) pendingReturns;

    event AuctionEnded(address winner, uint highestBid);

    /// Modifiers are a convenient way to validate inputs to
    /// functions. `onlyBefore` is applied to `bid` below:
    /// The new function body is the modifier's body where
    /// `_` is replaced by the old function body.
    modifier onlyBefore(uint _time) { require(now < _time); _; }
    modifier onlyAfter(uint _time) { require(now > _time); _; }

    constructor(
        uint _biddingTime,
        uint _revealTime,
        address payable _beneficiary
    ) public {
        beneficiary = _beneficiary;
        biddingEnd = now + _biddingTime;
        revealEnd = biddingEnd + _revealTime;
    }

    /// Place a blinded bid with `_blindedBid` =
    /// keccak256(abi.encodePacked(value, fake, secret)).
    /// The sent ether is only refunded if the bid is correctly
    /// revealed in the revealing phase. The bid is valid if the
    /// ether sent together with the bid is at least "value" and
    /// "fake" is not true. Setting "fake" to true and sending
    /// not the exact amount are ways to hide the real bid but
    /// still make the required deposit. The same address can
    /// place multiple bids.
    function bid(bytes32 _blindedBid)
        public
        payable
        onlyBefore(biddingEnd)
    {
        bids[msg.sender].push(Bid({
            blindedBid: _blindedBid,
            deposit: msg.value
        }));
    }

    /// Reveal your blinded bids. You will get a refund for all
    /// correctly blinded invalid bids and for all bids except for
    /// the totally highest.
    function reveal(
        uint[] memory _values,
        bool[] memory _fake,
        bytes32[] memory _secret
    )
        public
        onlyAfter(biddingEnd)
        onlyBefore(revealEnd)
    {
        uint length = bids[msg.sender].length;
        require(_values.length == length);
        require(_fake.length == length);
        require(_secret.length == length);

        uint refund;
        for (uint i = 0; i < length; i++) {
            Bid storage bidToCheck = bids[msg.sender][i];
            (uint value, bool fake, bytes32 secret) =
                    (_values[i], _fake[i], _secret[i]);
            if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) {
                // Bid was not actually revealed.
                // Do not refund deposit.
                continue;
            }
            refund += bidToCheck.deposit;
            if (!fake && bidToCheck.deposit >= value) {
                if (placeBid(msg.sender, value))
                    refund -= value;
            }
            // Make it impossible for the sender to re-claim
            // the same deposit.
            bidToCheck.blindedBid = bytes32(0);
        }
        msg.sender.transfer(refund);
    }

    // This is an "internal" function which means that it
    // can only be called from the contract itself (or from
    // derived contracts).
    function placeBid(address bidder, uint value) internal
            returns (bool success)
    {
        if (value <= highestBid) {
            return false;
        }
        if (highestBidder != address(0)) {
            // Refund the previously highest bidder.
            pendingReturns[highestBidder] += highestBid;
        }
        highestBid = value;
        highestBidder = bidder;
        return true;
    }

    /// Withdraw a bid that was overbid.
    function withdraw() public {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {
            // It is important to set this to zero because the recipient
            // can call this function again as part of the receiving call
            // before `transfer` returns (see the remark above about
            // conditions -> effects -> interaction).
            pendingReturns[msg.sender] = 0;

            msg.sender.transfer(amount);
        }
    }

    /// End the auction and send the highest bid
    /// to the beneficiary.
    function auctionEnd()
        public
        onlyAfter(revealEnd)
    {
        require(!ended);
        emit AuctionEnded(highestBidder, highestBid);
        ended = true;
        beneficiary.transfer(highestBid);
    }
}

Safe Remote Purchase

pragma solidity >=0.4.22 <0.6.0;

contract Purchase {
    uint public value;
    address payable public seller;
    address payable public buyer;
    enum State { Created, Locked, Inactive }
    State public state;

    // Ensure that `msg.value` is an even number.
    // Division will truncate if it is an odd number.
    // Check via multiplication that it wasn't an odd number.
    constructor() public payable {
        seller = msg.sender;
        value = msg.value / 2;
        require((2 * value) == msg.value, "Value has to be even.");
    }

    modifier condition(bool _condition) {
        require(_condition);
        _;
    }

    modifier onlyBuyer() {
        require(
            msg.sender == buyer,
            "Only buyer can call this."
        );
        _;
    }

    modifier onlySeller() {
        require(
            msg.sender == seller,
            "Only seller can call this."
        );
        _;
    }

    modifier inState(State _state) {
        require(
            state == _state,
            "Invalid state."
        );
        _;
    }

    event Aborted();
    event PurchaseConfirmed();
    event ItemReceived();

    /// Abort the purchase and reclaim the ether.
    /// Can only be called by the seller before
    /// the contract is locked.
    function abort()
        public
        onlySeller
        inState(State.Created)
    {
        emit Aborted();
        state = State.Inactive;
        seller.transfer(address(this).balance);
    }

    /// Confirm the purchase as buyer.
    /// Transaction has to include `2 * value` ether.
    /// The ether will be locked until confirmReceived
    /// is called.
    function confirmPurchase()
        public
        inState(State.Created)
        condition(msg.value == (2 * value))
        payable
    {
        emit PurchaseConfirmed();
        buyer = msg.sender;
        state = State.Locked;
    }

    /// Confirm that you (the buyer) received the item.
    /// This will release the locked ether.
    function confirmReceived()
        public
        onlyBuyer
        inState(State.Locked)
    {
        emit ItemReceived();
        // It is important to change the state first because
        // otherwise, the contracts called using `send` below
        // can call in again here.
        state = State.Inactive;

        // NOTE: This actually allows both the buyer and the seller to
        // block the refund - the withdraw pattern should be used.

        buyer.transfer(value);
        seller.transfer(address(this).balance);
    }
}

Micropayment Channel

このセクションではペイメントチャンネルの実行をどうやって行うかを学びます。ここでは同じ人の間で繰り返し行われるEtherの送金を安全、高速かつトランザクションの手数料なしに行うために暗号化された署名を使います。 この例では、どの様に署名、検証し、そしてどの様にペイメントチャンネルをセットアップするかを理解する必要があります。

Creating and verifying signatures

アリスがボブにある量のEtherを送りたいという状況を想像してください。

アリスは暗号学的に署名されたメッセージをオフチェーン(例えばEメール)でボブに送る必要があります。これはチェックを書く時に似ています。

アリスとボブはトランザクションを許可するために署名を使います。これはEthereum上のスマートコントラクトで可能です。 アリスはEtherを送るシンプルなスマートコントラクトを作りました。しかし、その送金を始めるためのファンクションの呼び出しを自分で行う代わりに、ボブにそれを行わせ、トランザクションの手数料を払わせます。

そのコントラクトは次の様に動作します:

  1. アリスは最終的に行われる支払いをカバーするのに十分な量のEtherを付与した上で ReceiverPays コントラクトをデプロイします。
  2. アリスは秘密鍵による署名により支払いを許可します。
  3. アリスは暗号学的に署名されたメッセージをボブに送ります。そのメッセージは隠される必要はありません(後で説明します)し、この送信メカニズムは重要ではありません。
  4. コントラクトに署名されたメッセージを渡すことでボブは支払いを要求します。コントラクトはメッセージの有効性を確認の上、コントラクト上の資金を解放します。

Creating the signature

アリスはトランザクションに署名するのにEthereumネットワークに繋げる必要はありません。このプロセスは完全にオフラインで行われます。このチュートリアルでは web3.jsMetaMask を使ってブラウザ上で署名を行います。さらに EIP-762 の中のメソッドで、他のセキュリティに関する恩恵を色々得られるメソッドを使用します。

/// Hashing first makes things easier
var hash = web3.utils.sha3("message to sign");
web3.eth.personal.sign(hash, web3.eth.defaultAccount, function () { console.log("Signed"); });

注釈

web3.eth.personal.sign は署名されたデータの長さを表しています。最初にハッシュ化されているため、メッセージは常に32バイトの長さで、この長さの接頭辞は常に同じです。

What to Sign

支払いを行うコントラクトでは署名されたメッセージは下記を含んでいる必要があります。

  1. 受領者のアドレス
  2. 送金額
  3. リプレイアタックに対する防御策

リプレイアタックは署名されたメッセージが承認を要求するのに再び使われることです。これを回避するためにEthereumのトランザクションと同じ様に、アカウントから送信されたトランザクションの数、いわゆるnonceを使用します。スマートコントラクトはnonceが何度も使われていないか確認します。

別のタイプのリプレイアタックはあるオーナーが支払いを行い、その後コントラクトを破棄する ReceiverPays というスマートコントラクトをデプロイした時に起こり得ます。その後、オーナーが再び RecipientPays をデプロイする時、その新しいコントラクトは前のデプロイでのnonceを知らないので、攻撃者は古いメッセージを再使用することができます。

アリスはメッセージの中にコントラクトのアドレスを含めることによりこの攻撃を防ぐことができます。さらにこの中ではコントラクトのアドレスを含んだメッセージだけが許可されます。このセクションの最後にあるコントラクトの中の claimPayment() ファンクションの中の最初の2行でこの例を確認できます。

Packing arguments

署名されたメッセージの中にどんな情報を含める必要があるか分かったので、メッセージをまとめ、ハッシュ化し、署名する準備ができました。 シンプルにするためにこのデータを連結させます。 ethereumjs-abi ライブラリは soliditySHA3 というファンクションを持っています。これは abi.encodePacked を使ってエンコードされた引数に適用されたSolidityの keccak256 と同じ振る舞いをします。以下は ReceiverPays で適切な署名を作成するJavaScriptのファンクションの例です。

// recipient is the address that should be paid.
// amount, in wei, specifies how much ether should be sent.
// nonce can be any unique number to prevent replay attacks
// contractAddress is used to prevent cross-contract replay attacks
function signPayment(recipient, amount, nonce, contractAddress, callback) {
    var hash = "0x" + abi.soliditySHA3(
        ["address", "uint256", "uint256", "address"],
        [recipient, amount, nonce, contractAddress]
    ).toString("hex");

    web3.eth.personal.sign(hash, web3.eth.defaultAccount, callback);
}

Recovering the Message Signer in Solidity

一般的にECDSA署名は rs という2つのパラメータで構成されています。Ethereum上の署名は3つ目のパラメータ v を含んでいます。これは署名のどのアカウントの秘密鍵が使われたか、トランザクションの送信者が誰か検証するのに使われます。

Solidityは rs そして v パラメータと一緒にメッセージを受け入れるビルトインファンクションの ecrecover を持っています。さらにこのファンクションはメッセージに署名したアドレスを返します。

Extracting the Signature Parameters

web3.jsでされた署名は rsv が連結されたものなので、最初のステップはこれをそれぞれに分けることです。これはクライアント側でもできますが、スマートコントラクト内ですれば1つのパラメータだけで済みます。バイト配列を要素ごとに分けるのは見た目があまり良くないので、splitSignature ファンクション(セクションの最後のフルコントラクトの3番目のファンクション)内でこの操作を行うために inline assembly を使用します。

Computing the Message Hash

スマートコントラクトはどのパラメータがサインされたか知る必要あるので、スマートコントラクトはパラメータをメッセージから再作成し、それを署名の認証に使わなければなりません。prefixedrecoverSigner ファンクションは claimPayment ファンクションの中でこれを行います。

The full contract

pragma solidity >=0.4.24 <0.6.0;

contract ReceiverPays {
    address owner = msg.sender;

    mapping(uint256 => bool) usedNonces;

    constructor() public payable {}

    function claimPayment(uint256 amount, uint256 nonce, bytes memory signature) public {
        require(!usedNonces[nonce]);
        usedNonces[nonce] = true;

        // this recreates the message that was signed on the client
        bytes32 message = prefixed(keccak256(abi.encodePacked(msg.sender, amount, nonce, this)));

        require(recoverSigner(message, signature) == owner);

        msg.sender.transfer(amount);
    }

    /// destroy the contract and reclaim the leftover funds.
    function kill() public {
        require(msg.sender == owner);
        selfdestruct(msg.sender);
    }

    /// signature methods.
    function splitSignature(bytes memory sig)
        internal
        pure
        returns (uint8 v, bytes32 r, bytes32 s)
    {
        require(sig.length == 65);

        assembly {
            // first 32 bytes, after the length prefix.
            r := mload(add(sig, 32))
            // second 32 bytes.
            s := mload(add(sig, 64))
            // final byte (first byte of the next 32 bytes).
            v := byte(0, mload(add(sig, 96)))
        }

        return (v, r, s);
    }

    function recoverSigner(bytes32 message, bytes memory sig)
        internal
        pure
        returns (address)
    {
        (uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);

        return ecrecover(message, v, r, s);
    }

    /// builds a prefixed hash to mimic the behavior of eth_sign.
    function prefixed(bytes32 hash) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
    }
}

Writing a Simple Payment Channel

アリスは今、シンプルですが完全なペイメントチャンネルを作っています。ペイメントチャンネルは繰り返されるEtherのやり取りを安全、即時、かつトランザクション手数料なしで行うため、暗号学的な署名を使用しています。アリスとボブによるシンプルな間接的ペイメントチャンネルを考えてみましょう。

What is a Payment Channel?

ペイメントチャンネルは参加者にトランザクションを使用しないで何度もEtherのやり取りをできる様にしています。つまりトランザクションに関わる遅れや手数料が発生しないということです。

  1. アリスはあるコントラクトにEtherでお金を入れました。これでペイメントチャンネルが"開きます"。
  2. アリスは何Etherが受領者に受け渡されるか書いてあるメッセージに署名しました。このステップは支払いごとに繰り返されます。
  3. ボブは支払われたEtherを引き出し、残りを送金者に返しペイメントチャンネルを"閉じました"。

注釈

ステップ1と3だけトランザクションが必要で、ステップ2では送金者が受領者にオフチェーンの方法(例えばEmail)で署名されたメッセージを送っているということです。つまりたった2つのトランザクションだけでいくらでも送金が行えるということです。

スマートコントラクトがエスクロー(第三者信託)としてEtherを扱い、そして有効に署名されたメッセージを引き受けているため、ボブはファンドされたお金を受け取れることが保証されています。スマートコントラクトは二人のペイメントチャンネルのタイムアウトを行うこともできるので、アリスは受領者がチャンネルのクローズを拒否してもお金が戻ってくることが保証されています。どのくらいペイメントチャンネルを開いておくかは参加者が決めることができます。短い期間のトランザクションでは例えばインターネットカフェで分ごとに課金される仕組みであったり、もっと長いもので言えば、時給で働く従業員への支払いに使えますし、ペイメントチャンネルは何ヶ月、何年もオープンにしておくことができます。

Opening the Payment Channel

ペイメントチャンネルを開くためにアリスはスマートコントラクトをデプロイしました。そのスマートコントラクトにはエスクローされるEtherを渡し、受領者とチャンネルの最大存続期間を決めました。これはこのセクションの最後にあるコントラストの中の SimplePaymentChannel ファンクションに入っています。

Making Payments

アリスはボブに署名されたメッセージを送ることで支払いを行います。このステップは完全にEthereumネットワークの外側で行われます。 メッセージは送信者により暗号化された署名が行われ、受領者に直接送られます。

それぞれのメッセージは以下の情報を含んでいます。
  • スマートコントラクトのアドレス(クロスコントラクト攻撃を防ぐため)
  • 現状受領者が受け取っているEtherの総額

ペイメントチャンネルは幾度と行われる送金の最後に一度だけクローズされます。 このため送られたメッセージの内、1つだけが履行されます。これが各マイクロペイメントの額ではなく累積額をメッセージにのせている理由です。最新のメッセージが最高額が書いてあるので、受領者は自然にそのメッセージを履行します。 スマートコントラクトは1つのメッセージだけを受け入れるため、メッセージごとのナンスはもう必要ありません。意図していた以外の他のチャンネルによって使われない様に、このスマートコントラクトのアドレスは使われたままです。

以下にメッセージに暗号学的に署名した前回のセクションから修正したJavaScriptを示します。

function constructPaymentMessage(contractAddress, amount) {
    return abi.soliditySHA3(
        ["address", "uint256"],
        [contractAddress, amount]
    );
}

function signMessage(message, callback) {
    web3.eth.personal.sign(
        "0x" + message.toString("hex"),
        web3.eth.defaultAccount,
        callback
    );
}

// contractAddress is used to prevent cross-contract replay attacks.
// amount, in wei, specifies how much Ether should be sent.

function signPayment(contractAddress, amount, callback) {
    var message = constructPaymentMessage(contractAddress, amount);
    signMessage(message, callback);
}

Closing the Payment Channel

ボブがチャンネルにあるお金を受け取る準備ができた時、スマートコントラクト内の close ファンクションを呼び出し、チャンネルをクローズする時間です。 チャンネルを閉じるときに、受領者に彼らがチャンネルに渡したEtherが支払われ、コントラクトは破棄されます。残っているEtherはアリスに返却されます。チャンネルを閉じるために、ボブはアリスによってサインされたメッセージを提供する必要があります。

スマートコントラクトはそのメッセージに送信者からの有効な署名がなされているか検証しなければなりません。この検証プロセスの目的は受領者が使うプロセスと同じです。このセクションの最後にあるSolidityの isValidSignaturerecoverSigner ファンクションは ReceiverPays コントラクトから借りてきたファンクションと共に、これらに対応する前セクションのJavascriptのファンクションと同様な動きをします。

一番最近のペイメントのメッセージを送ったペイメントチャンネルの受領者のみが close ファンクションを呼ぶことができます。なぜならそのメッセージが一番高い合計の金額を持っているからです。もし送信者がこのファンクションを呼ぶ権限を持っていると、その送信者が低い総額のメッセージを作って、受領者が受け取るべき金額を改ざんできてしまいます。

そのファンクションは署名されたメッセージが与えられたパラメータと合っているか検証します。全ての検証が通ったら、受領者は取り分のEtherを受け取り、送信者が selfdestruct を通じて残りを受け取ります。フルコントラクト内で close ファンクションは見ることができます。

Channel Expiration

ボブはペイメントチャンネルをいつでも閉じることができます。しかしチャンネルが閉じられなかった場合、アリスは第三者預託されたお金を回収する方法が必要になります。コントラクトのデプロイ時にコントラクトの失効期日がセットされます。その日になると、アリスはお金を回収するために claimTimeout を呼ぶことができます。claimTimeout ファンクションはフルコントラクト内で確認できます。

The full contract

pragma solidity >=0.4.24 <0.6.0;

contract SimplePaymentChannel {
    address payable public sender;      // The account sending payments.
    address payable public recipient;   // The account receiving the payments.
    uint256 public expiration;  // Timeout in case the recipient never closes.

    constructor (address payable _recipient, uint256 duration)
        public
        payable
    {
        sender = msg.sender;
        recipient = _recipient;
        expiration = now + duration;
    }

    function isValidSignature(uint256 amount, bytes memory signature)
        internal
        view
        returns (bool)
    {
        bytes32 message = prefixed(keccak256(abi.encodePacked(this, amount)));

        // check that the signature is from the payment sender
        return recoverSigner(message, signature) == sender;
    }

    /// the recipient can close the channel at any time by presenting a
    /// signed amount from the sender. the recipient will be sent that amount,
    /// and the remainder will go back to the sender
    function close(uint256 amount, bytes memory signature) public {
        require(msg.sender == recipient);
        require(isValidSignature(amount, signature));

        recipient.transfer(amount);
        selfdestruct(sender);
    }

    /// the sender can extend the expiration at any time
    function extend(uint256 newExpiration) public {
        require(msg.sender == sender);
        require(newExpiration > expiration);

        expiration = newExpiration;
    }

    /// if the timeout is reached without the recipient closing the channel,
    /// then the Ether is released back to the sender.
    function claimTimeout() public {
        require(now >= expiration);
        selfdestruct(sender);
    }

    /// All functions below this are just taken from the chapter
    /// 'creating and verifying signatures' chapter.

    function splitSignature(bytes memory sig)
        internal
        pure
        returns (uint8 v, bytes32 r, bytes32 s)
    {
        require(sig.length == 65);

        assembly {
            // first 32 bytes, after the length prefix
            r := mload(add(sig, 32))
            // second 32 bytes
            s := mload(add(sig, 64))
            // final byte (first byte of the next 32 bytes)
            v := byte(0, mload(add(sig, 96)))
        }

        return (v, r, s);
    }

    function recoverSigner(bytes32 message, bytes memory sig)
        internal
        pure
        returns (address)
    {
        (uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);

        return ecrecover(message, v, r, s);
    }

    /// builds a prefixed hash to mimic the behavior of eth_sign.
    function prefixed(bytes32 hash) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
    }
}

注釈

splitSignature ファンクションは全てのセキュリティチェックを使いません。本当の実行時にはもっと厳しくテストされたopenzepplin's version の様なライブラリを使用するべきです。

Verifying Payments

前のセクションとは違い、ペイメントチャンネル内のメッセージはすぐには履行されません。受領者が最新のメッセージを確認し続け、ペイメントチャンネルを閉じるときにそのメッセージを履行します。つまり受領者が各メッセージの検証をすることが重要ということです。 そうしないと、受領者が最終的にお金を受け取れる保証がされなくなります。

受領者は下記のプロセスで各メッセージを検証すべきです。

  1. メッセージの中のコントラクトアドレスがペイメントチャンネルと合っているか検証してください。
  2. 新しい総額が予定していたものと同じか検証してください。
  3. 新しい総額が第三者預託されたEtherを超えていないか検証してください。
  4. 署名が有効か、そしてペイメントチャンネルの送信者からのものか検証してください。

この検証を記載するために ethereumjs-util ライブラリを使用します。最終ステップは色々な方法で行うことができますが、ここではJavaScriptを使用します。次のコードは上記の JavaScriptコード の署名から constructMessage ファンクションを借りています。

// this mimics the prefixing behavior of the eth_sign JSON-RPC method.
function prefixed(hash) {
    return ethereumjs.ABI.soliditySHA3(
        ["string", "bytes32"],
        ["\x19Ethereum Signed Message:\n32", hash]
    );
}

function recoverSigner(message, signature) {
    var split = ethereumjs.Util.fromRpcSig(signature);
    var publicKey = ethereumjs.Util.ecrecover(message, split.v, split.r, split.s);
    var signer = ethereumjs.Util.pubToAddress(publicKey).toString("hex");
    return signer;
}

function isValidSignature(contractAddress, amount, signature, expectedSigner) {
    var message = prefixed(constructPaymentMessage(contractAddress, amount));
    var signer = recoverSigner(message, signature);
    return signer.toLowerCase() ==
        ethereumjs.Util.stripHexPrefix(expectedSigner).toLowerCase();
}