如何写一个简单的以太坊支付通道

在这篇文章中,我将实现一个简单但完整的以太坊支付通道。支付通道使用密码签名,以安全、即时、无交易费用重复地传送Ether。

什么是支付通道?

以太坊交易提供了一种安全的方式来转账,但每个交易需要被包括在一个区块中和并被挖掘。这意味着交易需要一些时间,并要求支付一些费用来补偿矿工的工作。特别是,这个交易费用使得其产生的这种小额支付,成为了以太坊和其他类似于它的区块链的使用,变得有点儿费劲一个原因。

支付通道允许参与者在不使用交易的情况下重复发送Ether。这意味着可以避免与交易相关的延迟和因此产生费用。在这篇文章中,我们将探讨一个简单的单向支付通道。这包括三个步骤:

  • 1.发送者用Ether支付一个智能合约。这会打开支付通道。
  • 2.发送者签署消息,指明该ether中应向接收者支付多少。对于每个支付,都重复这一步骤。
  • 3.接收者关闭支付通道,收取他们的那部分ether,并将其余部分返回发送者。

重要的是,只有步骤1和步骤3需要空缺交易。步骤2通过密码签名和两方之间的通信(如电子邮件)完成。这意味着只需要两个交易来支持任何数量的发送。

收件人保证收到他们的资金,因为智能合约托管了ether并认可有效签署的消息。智能合约还强制执行直到截止时间,而且发送方有权收回资金,即使接收方拒绝关闭支付通道。

这取决于支付通道的参与者决定多长时间保持开放。对于短时间的交互,例如对于提供网络服务按每分钟支付的网吧,使用只持续一个小时左右的支付通道就足够了。对于一个较长期的支付关系,比如给员工支付按小时计的工资,支付通道可以持续数月或数年。

打开支付通道

为了打开支付通道,发送方部署智能合约,ether也将被托管,并指定接收方和通道存在的最晚截止时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

支付款项

发送者通过向接收者发送消息来进行支付。该步骤完全在以太坊网络之外执行。消息由发送方进行加密签名,然后直接发送给接收方。

每个消息包括以下信息:

  • 智能合约的地址,用来防止跨合约replay攻击。
  • 迄今为止,接受者所消耗的ether总量。

在一系列转账结束时,支付通道只关闭一次。正因为如此,只有一个发送的消息将被赎回。这就是为什么每个消息都指定了累积的Ether消耗总量,而不是单个微支付的量。接收者自然会选择赎回最近的消息,因为这是一个总拥有最高ether的消息。

请注意,因为智能合约仅对单个消息进行维护,所以不需要每个临时消息。智能合约的地址仍然用于防止用于一个支付通道的消息被用于不同的通道。

可以用支持加密的hash和签名操作的任何语言构建和签名支付相应的消息。下面的代码是用JavaScript编写的,并且使用ethereumjs-abi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
}

核实付款

与签名不同,支付通道中的消息不会立即被赎回。接收方跟踪最新消息并在关闭支付通道时赎回。这意味着接收方对每个消息进行自己的验证是至关重要的。否则,不能保证收件人最终能得到报酬。

接收方应使用以下过程验证每个消息:

  • 1.验证消息中的合约地址与支付通道相匹配。
  • 2.验证新合计是否为预期金额。
  • 3.验证新的总量不超过ether的量。
  • 4.验证签名是否有效,并来自支付通道发送者。

前三个步骤很简单。最后一步可以通过多种方式执行,但是如果它在JavaScript中完成,我推荐ethereumjs-util库。下面的代码从上面的签名代码中借用constructMessage函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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回发送者。要关闭通道,接收方需要共享由发送方签名的消息。

智能合约必须验证消息包含来自发送者的有效签名。进行此验证的过程与接收方使用的过程相同。isValidSignaturerecoverSigner函数与前一部分中的JavaScript代码对应。后者是在Signing and Verifying Messages in Ethereum中从ReceiverPays合约中copy来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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来恢复其资金。

1
2
3
4
5
6
// 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,所以接收者在到达期满之前关闭通道是很重要的。

总结

  • 支付通道支持安全的、区块链外的资金转移,同时避免每次转账产生交易费用。
  • 付款是累积的,只有一个是在关闭频道时赎回的。
  • 转账是通过托管资金和密码签名来保证的。
  • 超时保护发送者的资金免受不合作的接收者的影响。

完整源代码见文末。

=========================================================================

如果你希望快速的开始使用.net和C#开发以太坊应用,那这个我们进行打造的课程会很有帮助:

C#以太坊

如果是其他语言开发以太坊应用的也可以参考以下教程:

  • java以太坊教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
  • 以太坊教程,主要介绍智能合约与dapp应用开发,适合入门。
  • 以太坊开发,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
  • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
  • php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和事件等内容。

汇智网原创,转载请标明出处。这里是原文

完整源代码,simplePaymentChannel.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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);
}
}