aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/solidity-by-example.rst487
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();
+ }