在這篇文章中,我將實現一個簡單但完整的以太坊支付通道。支付通道使用密碼簽名,以安全、即時、無交易費用重複地傳送Ether。
什麼是支付通道?
以太坊交易提供了一種安全的方式來轉賬,但每個交易需要被包括在一個區塊中和並被挖掘。這意味着交易需要一些時間,並要求支付一些費用來補償礦工的工作。特別是,這個交易費用使得其產生的這種小額支付,成爲了以太坊和其他類似於它的區塊鏈的使用,變得有點兒費勁一個原因。
支付通道允許參與者在不使用交易的情況下重複發送Ether。這意味着可以避免與交易相關的延遲和因此產生費用。在這篇文章中,我們將探討一個簡單的單向支付通道。這包括三個步驟:
- 1.發送者用Ether支付一個智能合約。這會打開支付通道。
- 2.發送者簽署消息,指明該ether中應向接收者支付多少。對於每個支付,都重複這一步驟。
- 3.接收者關閉支付通道,收取他們的那部分ether,並將其餘部分返回發送者。
重要的是,只有步驟1和步驟3需要空缺交易。步驟2通過密碼簽名和兩方之間的通信(如電子郵件)完成。這意味着只需要兩個交易來支持任何數量的發送。
收件人保證收到他們的資金,因爲智能合約託管了ether並認可有效簽署的消息。智能合約還強制執行直到截止時間,而且發送方有權收回資金,即使接收方拒絕關閉支付通道。
這取決於支付通道的參與者決定多長時間保持開放。對於短時間的交互,例如對於提供網絡服務按每分鐘支付的網吧,使用只持續一個小時左右的支付通道就足夠了。對於一個較長期的支付關係,比如給員工支付按小時計的工資,支付通道可以持續數月或數年。
打開支付通道
爲了打開支付通道,發送方部署智能合約,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;
}
支付款項
發送者通過向接收者發送消息來進行支付。該步驟完全在以太坊網絡之外執行。消息由發送方進行加密簽名,然後直接發送給接收方。
每個消息包括以下信息:
- 智能合約的地址,用來防止跨合約replay攻擊。
- 迄今爲止,接受者所消耗的ether總量。
在一系列轉賬結束時,支付通道只關閉一次。正因爲如此,只有一個發送的消息將被贖回。這就是爲什麼每個消息都指定了累積的Ether消耗總量,而不是單個微支付的量。接收者自然會選擇贖回最近的消息,因爲這是一個總擁有最高ether的消息。
請注意,因爲智能合約僅對單個消息進行維護,所以不需要每個臨時消息。智能合約的地址仍然用於防止用於一個支付通道的消息被用於不同的通道。
可以用支持加密的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);
}
覈實付款
與簽名不同,支付通道中的消息不會立即被贖回。接收方跟蹤最新消息並在關閉支付通道時贖回。這意味着接收方對每個消息進行自己的驗證是至關重要的。否則,不能保證收件人最終能得到報酬。
接收方應使用以下過程驗證每個消息:
- 1.驗證消息中的合約地址與支付通道相匹配。
- 2.驗證新合計是否爲預期金額。
- 3.驗證新的總量不超過ether的量。
- 4.驗證簽名是否有效,並來自支付通道發送者。
前三個步驟很簡單。最後一步可以通過多種方式執行,但是如果它在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#開發以太坊應用,那這個我們進行打造的課程會很有幫助:
如果是其他語言開發以太坊應用的也可以參考以下教程:
匯智網原創,轉載請標明出處。這裏是原文