在这篇文章中,我将实现一个简单但完整的以太坊支付通道。支付通道使用密码签名,以安全、即时、无交易费用重复地传送Ether。php
以太坊交易提供了一种安全的方式来转帐,但每一个交易须要被包括在一个区块中和并被挖掘。这意味着交易须要一些时间,并要求支付一些费用来补偿矿工的工做。特别是,这个交易费用使得其产生的这种小额支付,成为了以太坊和其余相似于它的区块链的使用,变得有点儿费劲一个缘由。java
支付通道容许参与者在不使用交易的状况下重复发送Ether。这意味着能够避免与交易相关的延迟和所以产生费用。在这篇文章中,咱们将探讨一个简单的单向支付通道。这包括三个步骤:node
重要的是,只有步骤1和步骤3须要空缺交易。步骤2经过密码签名和两方之间的通讯(如电子邮件)完成。这意味着只须要两个交易来支持任何数量的发送。python
收件人保证收到他们的资金,由于智能合约托管了ether并承认有效签署的消息。智能合约还强制执行直到截止时间,并且发送方有权收回资金,即便接收方拒绝关闭支付通道。android
这取决于支付通道的参与者决定多长时间保持开放。对于短期的交互,例如对于提供网络服务按每分钟支付的网吧,使用只持续一个小时左右的支付通道就足够了。对于一个较长期的支付关系,好比给员工支付按小时计的工资,支付通道能够持续数月或数年。git
为了打开支付通道,发送方部署智能合约,ether也将被托管,并指定接收方和通道存在的最晚截止时间。程序员
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. function SimplePaymentChannel(address _recipient, uint256 duration) public payable { sender = msg.sender; recipient = _recipient; expiration = now + duration; }
发送者经过向接收者发送消息来进行支付。该步骤彻底在以太坊网络以外执行。消息由发送方进行加密签名,而后直接发送给接收方。github
每一个消息包括如下信息:web
在一系列转帐结束时,支付通道只关闭一次。正由于如此,只有一个发送的消息将被赎回。这就是为何每一个消息都指定了累积的Ether消耗总量,而不是单个微支付的量。接收者天然会选择赎回最近的消息,由于这是一个总拥有最高ether的消息。mongodb
请注意,由于智能合约仅对单个消息进行维护,因此不须要每一个临时消息。智能合约的地址仍然用于防止用于一个支付通道的消息被用于不一样的通道。
能够用支持加密的hash和签名操做的任何语言构建和签名支付相应的消息。下面的代码是用JavaScript编写的,而且使用ethereumjs-abi:
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); }
与签名不一样,支付通道中的消息不会当即被赎回。接收方跟踪最新消息并在关闭支付通道时赎回。这意味着接收方对每一个消息进行本身的验证是相当重要的。不然,不能保证收件人最终能获得报酬。
接收方应使用如下过程验证每一个消息:
前三个步骤很简单。最后一步能够经过多种方式执行,可是若是它在JavaScript中完成,我推荐ethereumjs-util库。下面的代码从上面的签名代码中借用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(); }
当接受者准备好接收他们的资金时,是时候经过在智能合约上调用close
功能来关闭支付通道。关闭通道给接收者,他们得到本身的ether并销毁合约,发送剩余的Ether回发送者。要关闭通道,接收方须要共享由发送方签名的消息。
智能合约必须验证消息包含来自发送者的有效签名。进行此验证的过程与接收方使用的过程相同。isValidSignature
和recoverSigner
函数与前一部分中的JavaScript代码对应。后者是在Signing and Verifying Messages in Ethereum中从ReceiverPays
合约中copy来的。
function isValidSignature(uint256 amount, bytes signature) internal view returns (bool) { bytes32 message = prefixed(keccak256(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); }
关闭功能只能由支付通道接收者来调用,而接收者天然会传递最新的支付消息,由于该消息具备最高的总费用。若是发送者被容许调用这个函数,他们能够提供一个较低费用的消息,并欺骗接收者。
函数验证签名的消息与给定的参数匹配。若是一切都被检测出来,收件人就发送了他们的部分ether,发送者经过selfdestruct
发送其他部分。
接收方能够在任什么时候候关闭支付通道,可是若是他们不这样作,发送者须要一种方法来收回他们的托管资金。在合约部署时设置了expiration
时间。一旦到达该时间,发送方能够调用claimTimeout
来恢复其资金。
// 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); }
在这个函数被调用以后,接收者不再能接收任何ether,因此接收者在到达期满以前关闭通道是很重要的。
完整源代码,simplePaymentChannel.sol
pragma solidity ^0.4.20; 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. function SimplePaymentChannel(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(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 released back to the sender. function claimTimeout() public { require(now >= expiration); selfdestruct(sender); } function splitSignature(bytes sig) internal pure returns (uint8, bytes32, bytes32) { require(sig.length == 65); bytes32 r; bytes32 s; uint8 v; 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; (v, r, 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("\x19Ethereum Signed Message:\n32", hash); } }
=========================================================================
若是你但愿快速的开始使用.net和C#开发以太坊应用,那这个咱们进行打造的课程会颇有帮助:
若是是其余语言开发以太坊应用的也能够参考如下教程:
这里是原文