diff options
author | chriseth <chris@ethereum.org> | 2018-07-10 21:34:36 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-07-10 21:34:36 +0800 |
commit | 4547b3234893a9f3949130a4e12d86be3b1c0de0 (patch) | |
tree | e542f5ad53914fa99514097f67ea9c4ce2384ac7 /docs | |
parent | 7650905567a0e6740b09e1d540d037152c0cac85 (diff) | |
parent | 15283e8535c6d1cedfe5d47f7bb1fbe03c77e207 (diff) | |
download | dexon-solidity-4547b3234893a9f3949130a4e12d86be3b1c0de0.tar dexon-solidity-4547b3234893a9f3949130a4e12d86be3b1c0de0.tar.gz dexon-solidity-4547b3234893a9f3949130a4e12d86be3b1c0de0.tar.bz2 dexon-solidity-4547b3234893a9f3949130a4e12d86be3b1c0de0.tar.lz dexon-solidity-4547b3234893a9f3949130a4e12d86be3b1c0de0.tar.xz dexon-solidity-4547b3234893a9f3949130a4e12d86be3b1c0de0.tar.zst dexon-solidity-4547b3234893a9f3949130a4e12d86be3b1c0de0.zip |
Merge pull request #4212 from jvmaia/patch-3
micropayment channel example with two chapters
Diffstat (limited to 'docs')
-rw-r--r-- | docs/solidity-by-example.rst | 487 |
1 files changed, 486 insertions, 1 deletions
diff --git a/docs/solidity-by-example.rst b/docs/solidity-by-example.rst index fcdd1862..07cb76c9 100644 --- a/docs/solidity-by-example.rst +++ b/docs/solidity-by-example.rst @@ -645,4 +645,489 @@ Safe Remote Purchase Micropayment Channel ******************** -To be written. +In this section we will learn how to build a simple implementation +of a payment channel. It use cryptographics signatures to make +repeated transfers of Ether between the same parties secure, instantaneous, and +without transaction fees. To do it we need to understand how to +sign and verify signatures, and setup the payment channel. + +Creating and verifying signatures +================================= + +Imagine Alice wants to send a quantity of Ether to Bob, i.e. +Alice is the sender and the Bob is the recipient. +Alice only needs to send cryptographically signed messages off-chain +(e.g. via email) to Bob and it will be very similar to writing checks. + +Signatures are used to authorize transactions, +and they are a general tool that is available to +smart contracts. Alice will build a simple +smart contract that lets her transmit Ether, but +in a unusual way, instead of calling a function herself +to initiate a payment, she will let Bob +do that, and therefore pay the transaction fee. +The contract will work as follows: + + 1. Alice deploys the ``ReceiverPays`` contract, attaching enough Ether to cover the payments that will be made. + 2. Alice authorizes a payment by signing a message with their private key. + 3. Alice sends the cryptographically signed message to Bob. The message does not need to be kept secret + (you will understand it later), and the mechanism for sending it does not matter. + 4. Bob claims their payment by presenting the signed message to the smart contract, it verifies the + authenticity of the message and then releases the funds. + +Creating the signature +---------------------- + +Alice does not need to interact with Ethereum network to +sign the transaction, the proccess is completely offline. +In this tutorial, we will sign messages in the browser +using ``web3.js`` and ``MetaMask``. +In particular, we will use the standard way described in `EIP-762 <https://github.com/ethereum/EIPs/pull/712>`_, +as it provides a number of other security benefits. + +:: + + /// Hashing first makes a few things easier + var hash = web3.sha3("message to sign"); + web3.personal.sign(hash, web3.eth.defaultAccount, function () {...}); + + +Note that the ``web3.personal.sign`` prepends the length of the message to the signed data. +Since we hash first, the message will always be exactly 32 bytes long, +and thus this length prefix is always the same, making everything easier. + +What to Sign +------------ + +For a contract that fulfills payments, the signed message must include: + + 1. The recipient's address + 2. The amount to be transferred + 3. Protection against replay attacks + +A replay attack is when a signed message is reused to claim authorization for +a second action. +To avoid replay attacks we will use the same as in Ethereum transactions +themselves, a so-called nonce, which is the number of transactions sent by an +account. +The smart contract will check if a nonce is used multiple times. + +There is another type of replay attacks, it occurs when the +owner deploys a ``ReceiverPays`` smart contract, performs some payments, +and then destroy the contract. Later, she decides to deploy the +``RecipientPays`` smart contract again, but the new contract does not +know the nonces used in the previous deployment, so the attacker +can use the old messages again. + +Alice can protect against it including +the contract's address in the message, and only +messages containing contract's address itself will be accepted. +This functionality can be found in the first two lines of the ``claimPayment()`` function in the full contract +at the end of this chapter. + +Packing arguments +----------------- + +Now that we have identified what information to include in the +signed message, we are ready to put the message together, hash it, +and sign it. For simplicity, we just concatenate the data. +The +`ethereumjs-abi <https://github.com/ethereumjs/ethereumjs-abi>`_ library provides +a function called ``soliditySHA3`` that mimics the behavior +of Solidity's ``keccak256`` function applied to arguments encoded +using ``abi.encodePacked``. +Putting it all together, here is a JavaScript function that +creates the proper signature for the ``ReceiverPays`` example: + +:: + + // 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); + } + +Recovering the Message Signer in Solidity +----------------------------------------- + +In general, ECDSA signatures consist of two parameters, ``r`` and ``s``. +Signatures in Ethereum include a third parameter called ``v``, that can be used +to recover which account's private key was used to sign in the message, +the transaction's sender. Solidity provides a built-in function +`ecrecover <mathematical-and-cryptographic-functions>`_ +that accepts a message along with the ``r``, ``s`` and ``v`` parameters and +returns the address that was used to sign the message. + +Extracting the Signature Parameters +----------------------------------- + +Signatures produced by web3.js are the concatenation of ``r``, ``s`` and ``v``, +so the first step is splitting those parameters back out. It can be done on the client, +but doing it inside the smart contract means only one signature parameter +needs to be sent rather than three. +Splitting apart a byte array into component parts is a little messy. +We will use `inline assembly <assembly>`_ to do the job +in the ``splitSignature`` function (the third function in the full contract +at the end of this chapter). + +Computing the Message Hash +-------------------------- + +The smart contract needs to know exactly what parameters were signed, +and so it must recreate the message from the parameters and use that +for signature verification. The functions ``prefixed`` and +``recoverSigner`` do this and their use can be found in the +``claimPayment`` function. + + +The full contract +----------------- + +:: + + pragma solidity ^0.4.24; + + contract ReceiverPays { + address owner = msg.sender; + + mapping(uint256 => bool) usedNonces; + + constructor() public payable {} + + function claimPayment(uint256 amount, uint256 nonce, bytes 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 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 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 +================================ + +Alice will now build a simple but complete implementation of a payment channel. +Payment channels use cryptographic signatures to make repeated transfers +of Ether securely, instantaneously, and without transaction fees. + +What is a Payment Channel? +-------------------------- + +Payment channels allow participants to make repeated transfers of Ether without +using transactions. This means that the delays and fees associated with transactions +can be avoided. We are going to explore a simple unidirectional payment channel between +two parties (Alice and Bob). Using it involves three steps: + + 1. Alice funds a smart contract with Ether. This "opens" the payment channel. + 2. Alice signs messages that specify how much of that Ether is owed to the recipient. This step is repeated for each payment. + 3. Bob "closes" the payment channel, withdrawing their portion of the Ether and sending the remainder back to the sender. + +Not ethat only steps 1 and 3 require Ethereum transactions, step 2 means that +the sender transmits a cryptographically signed message to the recipient via off chain ways (e.g. email). +This means only two transactions are required to support any number of transfers. + +Bob is guaranteed to receive their funds because the smart contract escrows +the Ether and honors a valid signed message. The smart contract also enforces a timeout, +so Alice is guaranteed to eventually recover their funds even if the recipient refuses +to close the channel. +It is up to the participants in a payment channel to decide how long to keep it open. +For a short-lived transaction, such as paying an internet cafe for each minute of network access, +or for a longer relationship, such as paying an employee an hourly wage, a payment could last for months or years. + +Opening the Payment Channel +--------------------------- + +To open the payment channel, Alice deploys the smart contract, +attaching the Ether to be escrowed and specifying the intendend recipient +and a maximum duration for the channel to exist. It is the function +``SimplePaymentChannel`` in the contract, that is at the end of this chapter. + +Making Payments +--------------- + +Alice makes payments by sending signed messages to Bob. +This step is performed entirely outside of the Ethereum network. +Messages are cryptographically signed by the sender and then transmitted directly to the recipient. + +Each message includes the following information: + + * The smart contract's address, used to prevent cross-contract replay attacks. + * The total amount of Ether that is owed the recipient so far. + +A payment channel is closed just once, at the of a series of transfers. +Because of this, only one of the messages sent will be redeemed. This is why +each message specifies a cumulative total amount of Ether owed, rather than the +amount of the individual micropayment. The recipient will naturally choose to +redeem the most recent message because that is the one with the highest total. +The nonce per-message is not needed anymore, because the smart contract will +only honor a single message. The address of the smart contract is still used +to prevent a message intended for one payment channel from being used for a different channel. + +Here is the modified javascript code to cryptographically sign a message from the previous chapter: + +:: + + 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); + } + + +Closing the Payment Channel +--------------------------- + +When Bob is ready to receive their funds, it is time to +close the payment channel by calling a ``close`` function on the smart contract. +Closing the channel pays the recipient the Ether they are owed and destroys the contract, +sending any remaining Ether back to Alice. +To close the channel, Bob needs to provide a message signed by Alice. + +The smart contract must verify that the message contains a valid signature from the sender. +The process for doing this verification is the same as the process the recipient uses. +The Solidity functions ``isValidSignature`` and ``recoverSigner`` work just like their +JavaScript counterparts in the previous section. The latter is borrowed from the +``ReceiverPays`` contract in the previous chapter. + +The ``close`` function can only be called by the payment channel recipient, +who will naturally pass the most recent payment message because that message +carries the highest total owed. If the sender were allowed to call this function, +they could provide a message with a lower amount and cheat the recipient out of what they are owed. + +The function verifies the signed message matches the given parameters. +If everything checks out, the recipient is sent their portion of the Ether, +and the sender is sent the rest via a ``selfdestruct``. +You can see the ``close`` function in the full contract. + +Channel Expiration +------------------- + +Bob can close the payment channel at any time, but if they fail to do so, +Alice needs a way to recover their escrowed funds. An *expiration* time was set +at the time of contract deployment. Once that time is reached, Alice can call +``claimTimeout`` to recover their funds. You can see the ``claimTimeout`` function in the +full contract. + +After this function is called, Bob can no longer receive any Ether, +so it is important that Bob closes the channel before the expiration is reached. + + +The full contract +----------------- + +:: + + pragma solidity ^0.4.24; + + contract SimplePaymentChannel { + address public sender; // The account sending payments. + address public recipient; // The account receiving the payments. + uint256 public expiration; // Timeout in case the recipient never closes. + + constructor (address _recipient, uint256 duration) + public + payable + { + sender = msg.sender; + recipient = _recipient; + expiration = now + duration; + } + + function isValidSignature(uint256 amount, bytes 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 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 realeased back to the sender. + function clainTimeout() public { + require(now >= expiration); + selfdestruct(sender); + } + + /// All functions below this are just taken from the chapter + /// 'creating and verifying signatures' chapter. + + function splitSignature(bytes 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 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)); + } + } + + +Note: The function ``splitSignature`` is very simple and does not use all security checks. +A real implementation should use a more rigorously tested library, such as +openzepplin's `version <https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/ECRecovery.sol>`_ of this code. + + + +Verifying Payments +------------------ + +Unlike in our previous chapter, messages in a payment channel aren't +redeemed right away. The recipient keeps track of the latest message and +redeems it when it's time to close the payment channel. This means it's +critical that the recipient perform their own verification of each message. +Otherwise there is no guarantee that the recipient will be able to get paid +in the end. + +The recipient should verify each message using the following process: + + 1. Verify that the contact address in the message matches the payment channel. + 2. Verify that the new total is the expected amount. + 3. Verify that the new total does not exceed the amount of Ether escrowed. + 4. Verify that the signature is valid and comes from the payment channel sender. + +We'll use the `ethereumjs-util <https://github.com/ethereumjs/ethereumjs-util>`_ +library to write this verifications. The final step can be done a number of ways, +but if it's being done in **JavaScript**. +The following code borrows the `constructMessage` function from the signing **JavaScript code** +above: + +:: + + // 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(); + } |