本文介绍了基于哈希时间锁的跨链资产交换,通过一套自动化Demo展示了以太坊和Fabric之间的资产交换。详细的Demo运行细节(包含下载Fabric容器、以太坊geth节点、启停Fabric和以太坊网络、部署htlc合约、运行htlc等操作)参考引用[5]指向的链接。
随着区块链技术的发展,各种具有不同特点,不同应用场景的区块链比如比特币、以太坊等公链以及私链和联盟链大量共存,它们之间相互独立,进行数据通信和价值转移仍面临挑战,区块链孤岛现象十分严重。为了解决区块链孤岛,实现链与链之间互联互通,进行价值转移,就必须实现跨链技术。就连银保监会陈伟刚也曾说过:“区块链的跨链需求会越来越多,因为区块链之一的联盟链,只是在自己的行业里应用,效益就得不到最优,更多的是行业与行业之间的联盟链跨链,所以跨链需求会成为区块链的一个拓展方向”。
目前,主流的跨链技术方案主要有三种,分别是哈希时间锁定、公证人机制和中继链方式。本篇主要分析实现哈希时间锁定跨链技术方案。哈希时间锁定本质是一种智能合约,其最先出现于2013年的BitcoinTalk论坛的一次讨论中,最早在闪电网络[1]中实现。
哈希时间锁是基于hash锁和时间锁来实现的。
哈希时间锁定是基于哈希锁和时间锁来实现跨链。假如有两条区块链ChainA和ChainB,每个链的原生资产分别为a、b,两个用户Alice和Bob,Alice有资产a,Bob有资产b,Alice与Bob想交换资产,那么他们可以使用以下的方式来实现:
通过以上的过程Alice和Bob就使用哈希时间锁在两条链上完成了资产转移。
虽然哈希时间锁定可以完成跨链,但它仅适用于不同区块链间的资产价值交换,且使用它的区块链网络需要如下三点:第一必须要有账户资产,第二必须通过可编程智能合约建立信任,第三交易双方必须在两种区块链网络中拥有各自的资产托管账户。哈希时间锁作为跨链资产交换解决方案,目前应用于WeCross[2]、闪电网络、Interledger、雷电网络、Sprites 通道等[3]。除此之外还在证券结算、去中心化交易所[4]以及央行数字货币跨境支付得到广泛引用。
因为哈希时间锁定不适用于fabric等没有资产的联盟链,所以我们需要先让我们的fabric网络拥有账户资产,为此我们需要实现账户资产链码,再实现哈希时间锁定链码,所以fabric网络需要账户资产和哈希时间锁定两种链码。对于以太坊,我们只需要编写一个哈希时间锁定合约即可。
type Account struct {
Address string `json:"address"` // 账户地址
Amount uint64 `json:"amount"` // 账户资产余额
Passwd string `json:"passwd"` // 账户密码
Sequence uint64 `json:"sequence"` // 账户序列号,用于创建中间账户
Type int `json:"type"` // 账户类型,0为中间账户1为普通账户
TransferTo [2]string `json:"transfer_to"` // 中间账户资产能够转出的地址,为哈希交易的双方地址
}
Account结构体为账户资产模型,其中Amount
为资产金额,Sequence
序列号主要用于创建中间账户,Type
为账户类型,0为中间账户,1为普通账户,TransferTo
表示该账户资产能够转入的账户地址,当为普通账户时为空,为中间账户时是hash交易的双方地址。
Account提供一个转账方法:
/**
*to:接收者账户
*amount:转账金额
*/
func (from *Account) Transfer(stub shim.ChaincodeStubInterface, to *Account, amount uint64) error {
sendKey := fmt.Sprintf(AccountPrefix, from.Address)
from.Amount = safeSub(from.Amount, amount)
receiverKey := fmt.Sprintf(AccountPrefix, to.Address)
to.Amount = safeAdd(to.Amount, amount)
...
}
账户结构体的转账方法,发送者账户减少资产接收者账户增加资产,在分别保存发送者账户和接收者账户。
账户资产链码主要提供4个接口,分别是中间用户注册、用户注册、资产转账和用户查询。
/**
*args[0]:address,中间账户地址。
*args[1]:passwd,中间账户的密码或者密码的哈希值。
*args[2]:flag,标志位,为hash表示为passwd为中间账户的密码的哈希值。
*args[3]:sender,哈希时间锁交易的发送者。
*args[4]:receiver,哈希时间锁交易的接收者。
*/
func (a *AccountAssert) createMidAccount(stub shim.ChaincodeStubInterface, args []string) pb.Response {
...
// 计算passwd的哈希值,以存储
if flag == "hash" {
pw = passwd
} else {
sha256Passwd := sha256.Sum256([]byte(passwd))
pw = hex.EncodeToString(sha256Passwd[:])
}
transferTo := [2]string{sender, receiver}
acc := Account{
Address: address,
Amount: 0,
Passwd: pw,
Sequence: 0,
Type: MidAccount,
TransferTo: transferTo,
}
...
}
中间用户注册需要5个参数,账户地址address、账户密码passwd、账户标志位flag、哈希时间锁交易发送者sender和哈希时间锁交易接收者receiver。中间账户的密码就是哈希时间锁定交易里面的哈希原像,因为可能为后创建哈希时间锁的账户只能知道哈希值,所以通过标志位flag区分passwd是哈希原像还是哈希值。
/**
*args[0]:address,普通账户地址。
*args[1]:passwd,普通账户的密码。
*/
func (a *AccountAssert) createAccount(stub shim.ChaincodeStubInterface, args []string) pb.Response {
...
passwd := args[1]
// 计算passwd的哈希值,以存储
sha256Passwd := sha256.Sum256([]byte(passwd))
acc := Account{
Address: address,
Amount: 0,
Passwd: hex.EncodeToString(sha256Passwd[:]),
Sequence: 0,
Type: GenAccount,
TransferTo: [2]string{}, // 普通账户,TransferTo为空值
}
...
}
用户注册需要传入地址address,密码passwd,密码存储的是哈希值。
/**
*args[0]:from,交易转出方地址。
*args[1]:to,交易转入方地址。
*args[2]:amount,转账金额。
*args[3]:passwd,交易转出方账户密码。
*/
func (a *Account) transfer(stub shim.ChaincodeStubInterface, args []string) pb.Response {
...
// 得到交易转出方账户
fromKey := fmt.Sprintf(AccountPrefix, from)
senderInfo, err := stub.GetState(fromKey)
sender := &Account{}
err = json.Unmarshal(senderInfo, sender)
...
// 验证交易转出方账户密码是否正确
sha256Passwd := sha256.Sum256([]byte(passwd))
if strings.Compare(sender.Passwd, hex.EncodeToString(sha256Passwd[:])) != 0 {
return shim.Error("sender account passwd error")
}
...
// 交易转出方是中间账户,判断转入方地址是否正确,保证中间账户资产的安全性
if sender.Type == MidAccount {
if sender.TransferTo[0] != to && sender.TransferTo[1] != to {
return shim.Error("receiving account doesn't match")
}
}
...
// 调用账户的转账方法执行转账操作
err = sender.Transfer(stub, receiver, amount)
...
}
资产转账需要传入发送者from,接收者to,金额amount和发送者密码passwd四个参数。首先判断发送者是否存在,再判断账户密码是否正确,接着再判断接收者是否存在以及发送者金额是否充足,当发送者是中间账户还要判断接收者地址的正确性,这些都校验通过最后在在发送者账户执行减法操作,在接收者账户执行加法操作,以完成转账。
/**
*args[0]:address,交易转出方地址。
*/
func (a *AccountAssert) query(stub shim.ChaincodeStubInterface, args []string) pb.Response {
...
key := fmt.Sprintf(AccountPrefix, address)
accByte, err := stub.GetState(key)
...
}
账户查询只需要传入账户地址,账户存在就返回账户信息,不存在就返回账户不存在。
type HTLC struct {
Sender string `json:"sender"`
Receiver string `json:"receiver"`
Amount uint64 `json:"amount"`
HashValue string `json:"hash_value"`
TimeLock int64 `json:"time_lock"`
PreImage string `json:"pre_image"`
LockAddress string `json:"lock_address"`
State HTLCState `json:"state"`
}
HTLC结构体是哈希时间锁定的交易数据结构,HashValue
为PreImage
的sha256哈希值,LockAddress
就是用户注册的中间账户地址,PreImage
为该中间账户的密码,状态State
共分为已锁定HashLOCK
、已领取Received
和已退款Refund
共三种。
HTLC链码主要有创建中间账户、创建哈希时间锁定、领取资产、退回资产和查询哈希时间锁定信息共5个功能。
/**
*args[0]:sender,哈希时间锁定交易发送者。
*args[1]:preImage,哈希时间锁定交易的哈希原像或者是原像的哈希值。
*args[2]:flag,标志位,为hash表示preImage为原像的哈希值。
*args[3]:receiver,哈希时间锁定交易接收者。
*/
func (h *HTLCChaincode) createMidAccount(stub shim.ChaincodeStubInterface, args []string) pb.Response {
...
// 中间账户地址以发送者地址+发送者的序列号方式创建的
midAddress := senderAccount.Address + uint64ToString(senderAccount.Sequence)
...
// 调用账号资产链码的创建中间账户接口
trans = [][]byte{[]byte("registermidaccount"), []byte(midAddress), []byte(preImage), []byte(flag), []byte(sender), []byte(receiver)}
resPonse = stub.InvokeChaincode(AccountChainCodeName, trans, AccountChainCodeChannel)
if resPonse.Status != shim.OK {
return shim.Error("register mid account error: " + resPonse.Message)
}
...
// 返回中间账户地址和哈希时间锁定交易的哈希值
respon := ResponseMidAccount{}
respon.Address = midAddress
if flag == "hash" {
respon.Hash = preImage
} else {
hashByte := sha256.Sum256([]byte(preImage))
respon.Hash = hex.EncodeToString(hashByte[:])
}
responByte, err := json.Marshal(respon)
...
return shim.Success(responByte)
}
创建中间账户需要传入交易发送者地址,哈希原像(哈希原像为中间账户的密码)或者哈希值,标志位和交易接收者地址。它主要调用账户资产链码的中间用户注册接口,返回的是中间账户地址和哈希时间锁定的哈希值。
/**
*args[0]:sender,哈希时间锁定交易发送者地址。
*args[1]:receiver,哈希时间锁定交易接收者地址。
*args[2]:amount,哈希时间锁定交易金额。
*args[3]:timelock,哈希时间锁定交易的时间锁。
*args[4]:hashValue,哈希时间锁交易哈希值。
*args[5]:passwd,哈希时间锁定交易发送者账户密码。
*args[6]:midaddress,哈希时间锁定交易的锁定地址。
*/
func (h *HTLCChaincode) createHash(stub shim.ChaincodeStubInterface, args []string) pb.Response {
...
// 调用账户资产链码的转账接口,完成资产从发送者账户到锁定账户的转移
trans = [][]byte{[]byte("transfer"), []byte(sender), []byte(midaddress), []byte(amountStr), []byte(passwd)}
resPonse = stub.InvokeChaincode(AccountChainCodeName, trans, AccountChainCodeChannel)
if resPonse.Status != shim.OK {
return shim.Error("create htlc transfer mid account error:" + resPonse.Message)
}
...
htlc := HTLC{
Sender: sender,
Receiver: receive,
Amount: amount,
HashValue: hashValue,
TimeLock: timeLock,
PreImage: "", // 先设置为空,在对方领取资产的时候在给值
LockAddress: midaddress,
State: HashLOCK,
}
htlcByte, err := json.Marshal(htlc)
idByte := sha256.Sum256(htlcByte)
id := hex.EncodeToString(idByte[:])
...
// 返回id
return shim.Success([]byte(id))
...
}
创建哈希时间锁定交易需要发送者地址、接收者地址、转账数额、时间锁、哈希值、发送者账户密码和中间账户地址。哈希值和中间账户地址就是上一步创建中间账户时的返回值。它主要调用账户资产链码的资产转账接口,完成资产从发送者账户到中间账户的转移,返回哈希时间锁定交易id。
/**
*args[0]:id,哈希时间锁定交易id。
*args[1]:preImage,哈希时间锁定的哈希原像。
*/
func (h *HTLCChaincode) withdraw(stub shim.ChaincodeStubInterface, args []string) pb.Response {
...
// 状态必须已锁定
if htlc.State != HashLOCK {
return shim.Error("htlc transaction state error")
}
// 时间锁不能过期
if htlc.TimeLock < time.Now().Unix() {
return shim.Error("time has expirated")
}
// 调用账户资产链码的转账接口,完成资产从中间账户到交易接收者账户的转移
trans := [][]byte{[]byte("transfer"), []byte(htlc.LockAddress), []byte(htlc.Receiver), []byte(uint64ToString(htlc.Amount)), []byte(preImage)}
resPonse := stub.InvokeChaincode(AccountChainCodeName, trans, AccountChainCodeChannel)
...
// 对哈希时间锁定交易的哈希原像进行赋值并把状态修改成已领取
htlc.PreImage = preImage
htlc.State = Received
...
}
领取资产需要哈希时间锁定交易id和哈希原像(中间账户密码),首先判断该哈希时间锁定交易是否存在,再判断哈希时间锁定状态是否是已锁定,然后判断时间锁是否超时,都检查通过之后再调用账户链码资产的资产转移接口,完成资产从中间账户到接收者账户的转移并更新哈希原像及状态。
/**
*args[0]:id,哈希时间锁定交易id。
*args[1]:preImage,哈希时间锁定的哈希原像。
*/
func (h *HTLCChaincode) refund(stub shim.ChaincodeStubInterface, args []string) pb.Response {
...
// 时间锁需要过期
if htlc.TimeLock > time.Now().Unix() {
return shim.Error("cannot refund assets before expiration")
}
// 状态要已锁定
if htlc.State != HashLOCK {
return shim.Error("htlc transaction state error")
}
// 调用账户资产链码的转账接口,完成资产从中间账户到交易发送者账户的转移
trans := [][]byte{[]byte("transfer"), []byte(htlc.LockAddress), []byte(htlc.Sender), []byte(uint64ToString(htlc.Amount)), []byte(preImage)}
resPonse := stub.InvokeChaincode(AccountChainCodeName, trans, AccountChainCodeChannel)
...
// 更新状态为已退款
htlc.State = Refund
...
}
退回资产是通过哈希时间锁定交易id和哈希原像,检查交易是否过期且状态是否为已锁定,检查通过再调用账户资产链码的资产转移接口,完成资产从中间账户到交易发送者账户的转移,之后再更新状态为已退款。
/**
*args[0]:id,哈希时间锁定交易id。
*/
func (h *HTLCChaincode) query(stub shim.ChaincodeStubInterface, args []string) pb.Response {
...
// 根据id查询对应的HTLC并返回
key := fmt.Sprintf(HTLCPrefix, id)
htlcByte, err := stub.GetState(key)
...
return shim.Success(htlcByte)
...
}
查询哈希时间锁定信息通过哈希时间锁定交易id来查看该笔哈希时间锁定交易的信息。
以太坊上的哈希时间锁定合约主要包含以下4个功能:资产锁定、提取资产、退回资产以及查询锁定信息。
/**
* @dev 发送者设置哈希时间锁来存储ETH以及其锁定时间。
*
* @param _receiver ETH接收者。
* @param _hashlock 基于sha256的哈希时间锁。
* @param _timelock 过期是的时间戳,在此之间之后若ETH还未被接收者提取,可以被发送者取回。
* @return htlcId 资产被锁定的在的HTLC的Id。之后的调用会需要。
*/
function newHTLC(address payable _receiver, bytes32 _hashlock, uint _timelock)
external
payable
fundsSent
futureTimelock(_timelock)
returns (bytes32 htlcId)
{
htlcId = sha256(abi.encodePacked(msg.sender, _receiver, msg.value, _hashlock, _timelock));
// Reject if a contract already exists with the same parameters. The
// sender must change one of these parameters to create a new distinct
// contract.
if (haveContract(htlcId))
revert("Contract already exists");
contracts[htlcId] = LockHTLC(msg.sender, _receiver, msg.value, _hashlock, _timelock, false, false, '0x0');
emit LogHTLCNew(htlcId, msg.sender, _receiver, msg.value, _hashlock, _timelock);
}
资产锁定操作需要接收者地址、哈希值、时间值、以及锁定的资产(ETH)数量,其中哈希值和时间值作为资产锁定的约束,即相关资产接收者需要在此时间值代表的时间之前提供正确的哈希原像才可以提取锁定的资产。资产发送者执行此操作会生成资产锁定id,并把资产发送者、接收者、锁定资产数量、哈希值、时间值、以及锁定id记录在日志中。
/**
* @dev 接收者一旦知道时间锁原像,会调用此方法提取锁定资产。
*
* @param _htlcId HTLC的Id。
* @param _preimage 哈希锁原像,sha256(_preimage) 等于哈希锁。
* @return bool 成功返回true。
*/
function withdraw(bytes32 _htlcId, bytes calldata _preimage)
external
contractExists(_htlcId)
hashlockMatches(_htlcId, _preimage)
withdrawable(_htlcId)
returns (bool)
{
LockHTLC storage c = contracts[_htlcId];
c.preimage = _preimage;
c.withdrawn = true;
c.receiver.transfer(c.amount);
emit LogHTLCWithdraw(_htlcId);
return true;
}
提取资产操作需要资产锁定id以及哈希原像,资产接收者在限定时间内执行此操作可以提取资产并且之前的资产锁定日志状态会被更新,资产提取也会记录在日志中;
/**
* @dev 如果时间锁过期,发送者调用此方法取回锁定的资产。
*
* @param _htlcId 锁定资产的HTLC的Id
* @return bool 成功返回true。
*/
function refund(bytes32 _htlcId)
external
contractExists(_htlcId)
refundable(_htlcId)
returns (bool)
{
LockHTLC storage c = contracts[_htlcId];
c.refunded = true;
c.sender.transfer(c.amount);
emit LogHTLCRefund(_htlcId);
return true;
}
退回资产操作需要资产锁定id,资产发送者在锁定时间过期后执行此操作会退回锁定的资产到自身账户,相关的退款操作也会记录在日志中。
/**
* @dev 获取HTLC的细节。
* @param _htlcId HTLC的Id。
* @return 所有LockHTLC的参数。
*/
function getContract(bytes32 _htlcId)
public
view
returns (
address sender,
address receiver,
uint amount,
bytes32 hashlock,
uint timelock,
bool withdrawn,
bool refunded,
bytes memory preimage
)
{
if (haveContract(_htlcId) == false){
bytes memory pi = '0x0';
return (address(0), address(0), 0, 0, 0, false, false, pi);
}
LockHTLC storage c = contracts[_htlcId];
return (
c.sender,
c.receiver,
c.amount,
c.hashlock,
c.timelock,
c.withdrawn,
c.refunded,
c.preimage
);
}
查询哈希锁定信息需要资产锁定id并返回该笔资产锁定信息。
跨链演示背景说明:假设有两个用户Alice和Bob,Alice在fabric网络上面有1000个资产,Bob没有资产,Alice在以太坊上面没有ETH,Bob在以太坊上面有ETH。现在Alice想用fabric网络上面的50个资产换1.5个ETH。
他们之间通过哈希锁定跨链交换资产流程(demo源码[5])主要包含以下两步(运行脚本参考引用[5]):
--------------------Create Alice Account on Fabric--------------------
INPUT:
Alice Account: alice
Alice PassWord: alicepasswd
OUTPUT:
Successfully Created Account
--------------------Create Bob Account on Fabric--------------------
INPUT:
Bob Account: alice
Bob PassWord: alicepasswd
OUTPUT:
Successfully Created Account
--------------------Using Faucet to Recharge 1000 Assets 2 Alice on Fabric--------------------
INPUT:
Alice Account: alice
Assets Amount: 1000
OUTPUT:
Transfer Succeeds
--------------------Query Alice Account on Fabric--------------------
INPUT:
Alice's Account Address: alice
OUTPUT:
Name: alice
Amount: 1000
--------------------Query Bob Account on Fabric--------------------
INPUT:
Bob's Account Address: bob
OUTPUT:
Name: bob
Amount: 0
--------------------Bob Deploy Eth Htlc Contract--------------------
INTPUT:
1. Bob's private key; 2. HTLC contract Abi; 3. HTLC contract Bytes Code
OUTPUT:
New Contract Address: 0x1022498a83Aca1AEA280e320bcb10f1d301dB5D5
--------------------Transfer a few Eth to Alice for Paying Transaction Service Fee--------------------
INPUT:
From: 0x93ee701C44f9aa98086685c3AC5810f79762202d
To: 0x87CDaCDACBCF9e560336A4352B7E89646A4402D1
Value: 0.01Eth
gasLimit: 150000
OUTPUT:
Successfully Transferred Fee 2 Alice, Transaction Hash Is: 0xcb3208ca5b0b22607d91e008c1eb1bf93e37e77ff76480b197616755ae5cb5f2
--------------------Get Alice's Balance on Ethereum--------------------
INPUT:
Alice's Address: 0x87CDaCDACBCF9e560336A4352B7E89646A4402D1
OUTPUT:
Alice's Balance: 0.01
--------------------Get Bob's Balance on Ethereum--------------------
INPUT:
Bob's Address: 0x93ee701C44f9aa98086685c3AC5810f79762202d
OUTPUT:
Bob's Balance: 119.99
--------------------Create Mid Account--------------------
INPUT:
sender: alice
receiver: bob
preimage: rootroot
OUTPUT:
Successfully Created MidAccount: alice0
HashLock: 0242c0436daa4c241ca8a793764b7dfb50c223121bb844cf49be670a3af4dd18
--------------------Lock Fabric Assets--------------------
INPUT:
Alice's Account: alice
Bob's Account: bob
Amount Fabric Assets to be Locked: 50
Expiration Duration: 30000000000
HashLock: 0242c0436daa4c241ca8a793764b7dfb50c223121bb844cf49be670a3af4dd18
AlicePasswd: alicepasswd
MidAccount: alice0
OUTPUT:
Successfully Locked Fabric Assets, HTLC ID: aba736cb52e6fdfd37d539605574e0f000216d026085a3f5f1d07a4f2406b192
--------------------Bob Lock Eth Asset--------------------
INPUT:
hashlock: 0x0242c0436daa4c241ca8a793764b7dfb50c223121bb844cf49be670a3af4dd18
expiration timestamp: 1632336156621
Amount of ETH(1ETH = 10^18wei) to be locked: 1.5
Bob's Address: 0x93ee701C44f9aa98086685c3AC5810f79762202d
OUTPUT:
Eth Has Been Locked. Generated HTLC ID Is: 0x0cddac0c8ca3e6f37367ea2ef6f5607c62c6886f098515543d4cfbfb80f69284
--------------------Alice Withdraw Eth--------------------
INPUT:
ETH HTLC ID: 0x0cddac0c8ca3e6f37367ea2ef6f5607c62c6886f098515543d4cfbfb80f69284
PreImage Bytes: 0x726f6f74726f6f74
Alice's Address and Private Key
OUTPUT:
Successfully Withdrew Eth, Preimage Hex Is: 0x726f6f74726f6f74
Convert Preimage Hex To String: rootroot
--------------------Query Alice's Balance on Ethereum--------------------
INPUT:
Alice's Address: 0x87CDaCDACBCF9e560336A4352B7E89646A4402D1
OUTPUT:
Alice's Balance: 1.5099999999809461
--------------------Query Bob's Balance on Ethereum--------------------
INPUT:
Bob's Address: 0x93ee701C44f9aa98086685c3AC5810f79762202d
OUTPUT:
Bob's Balance: 192.4900000000190539
--------------------Withdraw Fabric Assets--------------------
INPUT:
Fabric HTLC ID: aba736cb52e6fdfd37d539605574e0f000216d026085a3f5f1d07a4f2406b192
preimage: rootroot
OUTPUT: Successfully Received HTLC Assets.
--------------------Query Alice's Amount on Fabric--------------------
INPUT:
Alice's Account: alice
OUTPUT:
Alice's Amount: 950
--------------------Query Bob's Amount on Fabric--------------------
INPUT:
Bob's Account: bob
OUTPUT:
Bob's Amount: 50
哈希时间锁定的应用场景只适用于资产或者Token的转移,比较适用于公链带有原生Token的领域,对于不包含资产托管账户(例如Fabric)的区块链需要借助智能合约来构建账户概念。
[2]https://wecross.readthedocs.io/zh_CN/latest/docs/routine/htlc.html
[3]https://www.chainnews.com/articles/365768981629.htm
[5]https://github.com/ehousecy/htlc-samples
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!