智能合約的投票、拍賣、遠程購買、付款

投票

以下合約相當複雜,但展示了很多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;
    }
}

可能的改進

目前,需要許多交易來將投票權分配給所有參與者。你能想到一個更好的方法嗎?

拍賣

在本節中,我們將展示在以太坊上創建一個完全盲目的拍賣合同是多麼容易。我們將從公開競價開始,每個人都可以看到所做的出價,然後將此合約延伸到盲目拍賣中,直到競標期結束才能看到實際出價。

簡單公開拍賣

以下簡單拍賣合約的一般概念是每個人都可以在競標期間發送出價。出價已包括匯款/以太幣,以便將投標人與其出價綁定。如果提高出價,那麼之前出價最高的出價者會收回她的錢。在投標期結束後,必須手動調用合同以便受益人收到他們的錢 - 合同無法自行激活。

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);
    }
}

盲目拍賣

之前的公開拍賣延伸至以下盲目拍賣。盲目拍賣的優勢在於,在競標期結束時沒有時間壓力。在透明的計算平臺上進行盲目拍賣可能聽起來像是一個矛盾,但加密技術得到了拯救。

競標期間,投標人實際上並沒有發送她的出價,而只是一個哈希版本。由於目前認爲實際上不可能找到其哈希值相等的兩個(足夠長的)值,因此投標人承諾投標。在投標期結束後,投標人必須公開他們的投標:他們發送未加密的價值,合約檢查哈希值是否與投標期間提供的值相同。

另一個挑戰是如何同時使拍賣 具有約束力和盲目性:防止投標人在贏得拍賣後不發送資金的唯一方法是讓她將其與投標一起發送。由於價值轉移不能在以太坊中被矇蔽,任何人都可以看到價值。

以下合約通過接受任何大於最高出價的值來解決此問題。由於這當然只能在顯示階段進行檢查,因此某些出價可能無效,這是有目的的(它甚至提供了一個明確的標記,用於設置高價值轉移的無效出價):投標人可以通過放置幾個高價或低無效出價。

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);
    }
}

安全遠程購買

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);
    }
}

小額支付渠道

在本節中,我們將學習如何構建支付渠道的簡單實現。它使用密碼簽名在同一方之間重複傳輸以太網,安全,即時,無需交易費用。爲此,我們需要了解如何簽名和驗證簽名,以及設置付款渠道。

創建和驗證簽名

想象一下,Alice希望向Bob發送一定數量的Ether,即Alice是發件人而Bob是收件人。Alice只需要通過離線(例如通過電子郵件)向Bob發送加密簽名的消息,這與編寫支票非常相似。

簽名用於授權交易,它們是智能合約可用的通用工具。愛麗絲將建立一個簡單的智能合約,讓她傳輸以太網,但是以一種不尋常的方式,她會讓鮑勃這樣做,而不是自己調用一個功能,因此支付交易費用。合同將如下工作:

  1. 愛麗絲部署ReceiverPays合同,附上足夠的以支付將要支付的款項。
  2. Alice通過使用其私鑰對郵件進行簽名來授權付款。
  3. Alice將加密簽名的消息發送給Bob。消息不需要保密,並且發送它的機制無關緊要。
  4. Bob通過向智能合約提交簽名的消息來聲明他們的付款,它驗證消息的真實性然後釋放資金。

創建簽名

Alice不需要與以太坊網絡交互來簽署交易,該過程完全脫機。在本教程中,我們將使用web3.js和在瀏覽器中對消息進行簽名MetaMask。特別是,我們將使用EIP-762中描述的標準方法,因爲它提供了許多其他安全優勢。

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

請注意,web3.personal.sign預先將消息的長度添加到簽名數據。由於我們首先進行哈希,因此消息總是精確地爲32個字節,因此這個長度前綴始終相同,使一切變得更容易。

簽到什麼

對於履行付款的合同,簽名的郵件必須包括:

  1. 收件人的地址
  2. 要轉移的金額
  3. 防止重播攻擊

重放攻擊是指重新使用已簽名的郵件來聲明第二個操作的授權。爲了避免重放攻擊,我們將使用與以太坊交易本身相同的方法,即所謂的nonce,即帳戶發送的交易數量。智能合約將檢查是否多次使用nonce。

還有另一種類型的重放攻擊,它發生在所有者部署ReceiverPays智能合約,執行一些付款,然後銷燬合約時。後來,她決定再次部署 RecipientPays智能合約,但新合同不知道先前部署中使用的nonce,因此攻擊者可以再次使用舊消息。

Alice可以保護它,包括郵件中的合約地址,並且只接受包含合約地址的郵件。此功能可以在claimPayment()本章末尾的完整合約中的函數的前兩行中找到。

打包參數

既然我們已經確定了要在簽名消息中包含哪些信息,我們就可以將消息放在一起,哈希並對其進行簽名。爲簡單起見,我們只是連接數據。該 ethereumjs-ABI庫提供了一個調用的函數soliditySHA3,模仿密實度的行爲keccak256功能適用於使用的編碼參數abi.encodePacked。總而言之,這是一個JavaScript函數,可以爲ReceiverPays示例創建正確的簽名:

// 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" + ethereumjs.ABI.soliditySHA3(
        ["address", "uint256", "uint256", "address"],
        [recipient, amount, nonce, contractAddress]
    ).toString("hex");

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

在Solidity中恢復消息簽名者

通常,ECDSA簽名由兩個參數組成,rs。以太坊中的簽名包括一個名爲的第三個參數v,可用於恢復用於登錄郵件的帳戶的私鑰,即交易的發件人。密實度提供了一個內置函數 ecrecover 接受的消息與沿rsv參數,並返回用於簽署的消息中的地址。

提取簽名參數

通過web3.js產生簽名的串接rs並且v,因此,第一個步驟是分裂的那些參數回來。它可以在客戶端上完成,但在智能合約中執行它意味着只需要發送一個簽名參數而不是三個。將字節數組拆分爲組件部分有點混亂。我們將使用內聯彙編來完成splitSignature函數中的工作(本章末尾的完整契約中的第三個函數)。

計算消息散列

智能合約需要確切地知道簽署了哪些參數,因此必須從參數重新創建消息並將其用於簽名驗證。可以在函數中找到函數prefixed和 recoverSigner執行此操作及其使用 claimPayment

完整合同

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));
    }
}

編寫簡單的支付渠道

Alice現在將構建一個簡單但完整的支付渠道實施。支付渠道使用加密簽名安全,即時地重複傳輸以太網,無需交易費用。

什麼是付款渠道?

支付渠道允許參與者在不使用交易的情況下重複轉移以太網。這意味着可以避免與交易相關的延遲和費用。我們將探討雙方(Alice和Bob)之間的簡單單向支付渠道。使用它涉及三個步驟:

  1. Alice與Ether合作提供智能合約。這將“打開”付款渠道。
  2. Alice會簽署一些消息,指明對接收者的欠款量。每次付款都會重複此步驟。
  3. Bob“關閉”支付渠道,撤回其部分以太網並將剩餘部分發送回發件人。

請注意,只有步驟1和3需要以太坊交易,步驟2意味着發件人通過離線方式(例如電子郵件)將加密簽名的消息發送給收件人。這意味着只需要兩個事務來支持任意數量的傳輸。

Bob保證收到他們的資金,因爲智能合約託管以太並尊重有效的簽名消息。智能合約還會執行超時,因此即使收件人拒絕關閉渠道,Alice也能保證最終收回資金。由付款渠道的參與者決定保持打開的時間。對於短期交易,例如向員工支付小時工資,支付可能持續數月或數年。

打開支付渠道

爲了打開支付渠道,Alice部署了智能合約,附加了要託管的以太網,並指定了對方收件人以及該渠道存在的最長持續時間。它是SimplePaymentChannel合同中的功能 ,即本章末尾。

付款

Alice通過向Bob發送簽名消息來進行付款。該步驟完全在以太坊網絡之外執行。郵件由發件人以加密方式簽名,然後直接傳輸給收件人。

每條消息都包含以下信息:

  • 智能合約的地址,用於防止交叉合同重播攻擊。
  • 到目前爲止,接收方所欠的以太網總量。

在一系列轉賬結束時,付款渠道僅關閉一次。因此,只有一條發送的郵件將被兌換。這就是爲什麼每條消息都指定了所欠歐元的累計總量,而不是單個小額支付的金額。收件人自然會選擇兌換最新消息,因爲這是總數最高的消息。不再需要nonce per-message,因爲智能合約只會尊重單個消息。智能合約的地址仍用於防止用於一個支付渠道的消息被用於不同的渠道。

以下是修改後的javascript代碼,用於對上一章中的消息進行加密簽名:

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

function signMessage(message, callback) {
    web3.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);
}

關閉支付渠道

當Bob準備好接收他們的資金時,是時候通過調用close智能合約上的功能來關閉支付渠道。關閉頻道會向收件人支付他們所欠的以太幣並銷燬合同,將任何剩餘的以太網發送給Alice。要關閉頻道,Bob需要提供Alice簽名的消息。

智能合約必須驗證郵件是否包含發件人的有效簽名。執行此驗證的過程與收件人使用的過程相同。Solidity的功能isValidSignaturerecoverSigner工作方式與上一節中的JavaScript對應功能相同。後者是從ReceiverPays前一章的合約借來的 。

close功能只能由支付渠道收件人調用,支付渠道收件人自然會傳遞最新的支付消息,因爲該消息的總欠款最高。如果允許發件人調用此函數,他們可以提供較低金額的郵件,並欺騙收件人的欠款。

該函數驗證簽名的消息是否與給定的參數匹配。如果一切都結束,收件人將被髮送他們的以太部分,發送者將通過一個發送給其餘的selfdestruct。您可以close在完整合約中查看該功能。

頻道過期

Bob可以隨時關閉支付渠道,但如果他們不這樣做,Alice需要一種方法來收回他們託管的資金。一個過期時間設置在合約部署的時間。一旦達到該時間,愛麗絲可以打電話 claimTimeout來收回他們的資金。您可以claimTimeout在完整合約中查看該功能。

調用此功能後,Bob無法再接收任何以太網,因此Bob必須在到期之前關閉該通道。

完整合同

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的此代碼版本

驗證付款

與上一章不同,付款渠道中的郵件不會立即兌換。收件人會跟蹤最新消息,並在關閉付款渠道時將其兌換。這意味着收件人對每條消息執行自己的驗證至關重要。否則,無法保證收件人最終能夠獲得報酬。

收件人應使用以下過程驗證每條消息:

  1. 驗證郵件中的聯繫地址是否與付款渠道中匹配。
  2. 驗證新總計是否爲預期金額。
  3. 驗證新總計不超過託管的以太網數量。
  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();
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章