fabric可以跨鏈嗎?

前言

今天公司讓我整理一個基於fabric的跨鏈的方案,之前沒怎麼接觸過跨鏈,在這裏記錄下自己的思路吧。

首先,先明白幾個概念。什麼是跨鏈?我的理解是跨鏈是跨channel。下面詳細說下我的理由:

  1. 回顧下fabric的啓動過程:創建證書,生成創世區塊,通道配置交易塊,創建通道,節點加入通道,安裝鏈碼,實例化鏈碼,鏈碼的調用。這個是完整的生命週期。
  2. 一個節點上可以安裝多個chaincode,且每個chaincode是一個賬本。
  3. 同一個通道中,所有的節點安裝的是相同的chaincode,所以每個節點都有完整的數據,不存在跨鏈之說。
  4. 綜上,跨鏈是指跨channel,因爲不同的channel擁有不同的賬本,跨鏈的本質是把一個鏈上的數據轉移到另外一條鏈上

跨鏈我們既可以在上層來做,也可以在chaincode層來做。經過查找我發現了一個InvokeChaincode方法,看着不錯,看上去是用來調用其他的chaincode的。

所以我設計如下的跨鏈方案:

簡單描述下:Org1中的peer1和ORG3中的peer3加入channel1,並且安裝Chaincode1,Org2中的peer2 和ORG3中的peer3加入channel2,並且安裝Chaincode2。
peer3這個節點是可以跨鏈的關鍵所在,因爲該節點同時擁有兩個通道的數據。

先整個簡易版的跨鏈流程:

  1. Chaincode1:UserA向UserPub轉移10元錢,UserPub把這筆錢標記爲已鎖定:
  2. Chaincode2:通過invokeChaincode查詢UserPub是否已經鎖定該筆錢。未鎖定,則終止該次跨鏈,並把資產轉回UserA。否則執行3
  3. Chaincode2:UserPub向UserB轉移10元錢,同時UserPub把這筆錢標記爲已轉移(注:該筆錢不可退回UserA。)
  4. 跨鏈完成

事情到這裏,並沒有完,上面的操作不是一個原子操作,所以我們必須要考慮事務性,如果中間步驟出錯,我們要將整個過程進行回滾,並且這是在分佈式的環境下完成的,哎,真的讓人頭大。


工慾善其事必先利其器,下面我們來搭建跨鏈所需的環境

1. 搭建跨鏈環境

1.1 生成證書

在開始之前,我們需要相應的搭建相應的開發環境,我是在fabric的源碼基礎上進行做的。基於 fabric v1.3.0
我的環境規劃是:Org1有1個peer節點,Org2有1個peer節點,Org3有1個節點,其中Org1和Org3加入channel1,安裝chaincode1,Org2和Org3加入channel2,安裝chaincode2。

下面我所改動的文件的詳細內容請參考:
https://github.com/Anapodoton/CrossChain

證書的生成我們需要修改如下配置文件:
crypto-config.yaml
docker-compose-e2e-template.yaml
docker-compose-base.yam
generateArtifacts.sh

我們需要添加第三個組織的相關信息,修改相應的端口號。

改動完成之後,我們可以使用cryptogen工具生成相應的證書文件,我們使用tree命令進行查看。

1.2 生成創世區塊,應用通道配置交易文件和錨節點配置更新交易文件

我們需要修改configtx.yaml文件和generateArtifacts.sh文件。

我們使用的主要工具是configtxgen工具。目的是生成系統通道的創世區塊,兩個應用通道channel1和channel2的配置交易文件,每個channel的每個組織都要生成錨節點配置更新交易文件。生成後的文件如下所示:

1.3 啓動相應的容器

我們首先可以使用docker-comppose-e2e來測試下網絡的聯通是否正常。

docker-compose -f docker-compose-e2e.yaml 看看網絡是否是正常的 ,不正常的要及時調整。

接下來,我們修改docker-compose-cli.yaml,我們使用fabric提供的fabric-tools鏡像來創建cli容器來代替SDK。

1.4 創建網絡

這裏主要使用的是script.sh來創建網絡,啓動orderer節點和peer節點。

我們創建channel1,channel2,把各個節點分別加入channel,更新錨節點,安裝鏈碼,實例化鏈碼。

上面的操作全部沒有錯誤後,我們就搭建好了跨鏈的環境了,這裏在逼逼一句,我們創建了兩個通道,每個通道兩個組織,其中Org3是其交集。下面可以正式的進行跨鏈了。

其實在前面的操作中,並不是一帆風順的,大家可以看到,需要修改的文件其實還是蠻多的,有一個地方出錯,網絡就啓動不了,建議大家分步進行運行,一步一步的解決問題,比如說,我在configtx.yaml文件中,ORG3的MSPTYPE指定成了idemix類型的,導致後面無論如何也驗證不過,通道無法創建成功。

簡單說下idemix,這個玩意是fabric v1.3 引入的一個新的特性,是用來用戶做隱私保護的,基於零知識證明的知識,這裏不在詳述,感興趣的可以參考:
fabric關於idemix的描述

2. 跨鏈關鍵技術

2.1 API解讀

找到fabric提供了這麼一個函數的文檔,我們先來看看。

invokechaincode

// InvokeChaincode documentation can be found in 
interfaces.gofunc (stub *ChaincodeStub) InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response {     
// Internally we handle chaincode name as a composite name     
    if channel != "" {          
    chaincodeName = chaincodeName + "/" + channel     
    }    
    return stub.handler.handleInvokeChaincode(chaincodeName, args, stub.ChannelId, stub.TxID)}

下面是官方的文檔說明:

// InvokeChaincode locally calls the specified chaincode `Invoke` using the
// same transaction context; that is, chaincode calling chaincode doesn't
// create a new transaction message.
// If the called chaincode is on the same channel, it simply adds the called
// chaincode read set and write set to the calling transaction.
// If the called chaincode is on a different channel,
// only the Response is returned to the calling chaincode; any PutState calls
// from the called chaincode will not have any effect on the ledger; that is,
// the called chaincode on a different channel will not have its read set
// and write set applied to the transaction. Only the calling chaincode's
// read set and write set will be applied to the transaction. Effectively
// the called chaincode on a different channel is a `Query`, which does not
// participate in state validation checks in subsequent commit phase.
// If `channel` is empty, the caller's channel is assumed.
InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response

上面的意思是說:
InvokeChaincode並不會創建一條新的交易,使用的是之前的transactionID。
如果調用的是相同通道的chaincode,返回的是調用者的chaincode的響應。僅僅會把被調用的chaincode的讀寫集添加到調用的transaction中。
如果被調用的chaincode在不同的通道中,任何PutState的調用都不會影響被調用chaincode的賬本。

再次翻譯下,相同的通道invokeChaincode可以讀可以寫,不同的通道invokeChaincode可以讀,不可以寫。(但是可以讀也是有前提的,二者必須有相同的共同的物理節點纔可以)。下面我們寫個demo來驗證下。

2.2 驗證

下面我簡單搭建一個測試網絡來進行驗證,還是兩個channel,channel2中的chaincode通過invokeChaincode方法嘗試調用chaincode1中的方法,我們來看看效果。

我們採用方案的核心是不同通道的Chaincode是否可以query? 需要在什麼樣的條件下纔可以進行query?

其中chaincode1是fabric/examples/chaincode/go/example02,chaincode2是fabric/examples/chaincode/go/example05

直接貼出queryByInvoke核心代碼:

f := "query"
    queryArgs := toChaincodeArgs(f, "a")
    // if chaincode being invoked is on the same channel,
    // then channel defaults to the current channel and args[2] can be "".
    // If the chaincode being called is on a different channel,
    // then you must specify the channel name in args[2]
    response := stub.InvokeChaincode(chaincodeName, queryArgs, channelName)

我們分別執行如下兩次查詢:
第一次:
peer chaincode query -C "channel1" -n mycc1 -c '{"Args":["query","a"]}'

結果如下:可以查到正確的結果。

我們再次查詢,在channel2上通過chaincode2中的queryByInvoke方法調用channel1的chaincode1中的query方法:

peer chaincode query -C "channel2" -n mycc2 -c '{"Args":["queryByInvoke","a","mycc1"]}'

結果如下所示:

我們成功的跨越通道查到了所需的數據。但是事情真的這麼完美嗎?如果兩個通道沒有公共的物理節點還可以嗎?我們再來測試下,這次我們的網絡是channel1中有peer1,channel2中有peer2,二者沒有共同節點,我們再次在channel2中InvokeChaincode Channel1中的代碼,廢話不再多說,我們直接來看調用的結果:

綜上:結論是不同的通道可以query,但前提必須是有共同的物理節點。

2.3 深入瞭解

下面的內容不是必須看的,我們來深入進去看看invokeChaincode到底是如何實現的。我們發現上面的代碼引用了fabric/core/chaincode/shim/interfaces.go中的ChaincodeStubInterface接口的InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response

該接口的實現在其同目錄下的Chaincode.go文件中,我們看其代碼:

// InvokeChaincode documentation can be found in interfaces.go
func (stub *ChaincodeStub) InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response {   
// Internally we handle chaincode name as a composite name    
if channel != "" {          
    chaincodeName = chaincodeName + "/" + channel    
}   
return stub.handler.handleInvokeChaincode(chaincodeName, args, stub.ChannelId, stub.TxID)}

該方法把chaincodeName和channel進行了拼接,同時傳入了ChannelId和TxID,二者是Orderer節點發送來的。然後調用了handleInvokeChaincode,我們在來看handleInvokeChaincode。在同目錄下的handler.go文件中。

/ handleInvokeChaincode communicates with the peer to invoke another chaincode.
func (handler *Handler) handleInvokeChaincode(chaincodeName string, args [][]byte, channelId string, txid string) pb.Response {
	//we constructed a valid object. No need to check for error
	payloadBytes, _ := proto.Marshal(&pb.ChaincodeSpec{ChaincodeId: &pb.ChaincodeID{Name: chaincodeName}, Input: &pb.ChaincodeInput{Args: args}})

	// Create the channel on which to communicate the response from validating peer
	var respChan chan pb.ChaincodeMessage
	var err error
	if respChan, err = handler.createChannel(channelId, txid); err != nil {
		return handler.createResponse(ERROR, []byte(err.Error()))
	}

	defer handler.deleteChannel(channelId, txid)

	// Send INVOKE_CHAINCODE message to peer chaincode support
	msg := &pb.ChaincodeMessage{Type: pb.ChaincodeMessage_INVOKE_CHAINCODE, Payload: payloadBytes, Txid: txid, ChannelId: channelId}
	chaincodeLogger.Debugf("[%s] Sending %s", shorttxid(msg.Txid), pb.ChaincodeMessage_INVOKE_CHAINCODE)

	var responseMsg pb.ChaincodeMessage

	if responseMsg, err = handler.sendReceive(msg, respChan); err != nil {
		errStr := fmt.Sprintf("[%s] error sending %s", shorttxid(msg.Txid), pb.ChaincodeMessage_INVOKE_CHAINCODE)
		chaincodeLogger.Error(errStr)
		return handler.createResponse(ERROR, []byte(errStr))
	}

	if responseMsg.Type.String() == pb.ChaincodeMessage_RESPONSE.String() {
		// Success response
		chaincodeLogger.Debugf("[%s] Received %s. Successfully invoked chaincode", shorttxid(responseMsg.Txid), pb.ChaincodeMessage_RESPONSE)
		respMsg := &pb.ChaincodeMessage{}
		if err := proto.Unmarshal(responseMsg.Payload, respMsg); err != nil {
			chaincodeLogger.Errorf("[%s] Error unmarshaling called chaincode response: %s", shorttxid(responseMsg.Txid), err)
			return handler.createResponse(ERROR, []byte(err.Error()))
		}
		if respMsg.Type == pb.ChaincodeMessage_COMPLETED {
			// Success response
			chaincodeLogger.Debugf("[%s] Received %s. Successfully invoked chaincode", shorttxid(responseMsg.Txid), pb.ChaincodeMessage_RESPONSE)
			res := &pb.Response{}
			if err = proto.Unmarshal(respMsg.Payload, res); err != nil {
				chaincodeLogger.Errorf("[%s] Error unmarshaling payload of response: %s", shorttxid(responseMsg.Txid), err)
				return handler.createResponse(ERROR, []byte(err.Error()))
			}
			return *res
		}
		chaincodeLogger.Errorf("[%s] Received %s. Error from chaincode", shorttxid(responseMsg.Txid), respMsg.Type)
		return handler.createResponse(ERROR, responseMsg.Payload)
	}
	if responseMsg.Type.String() == pb.ChaincodeMessage_ERROR.String() {
		// Error response
		chaincodeLogger.Errorf("[%s] Received %s.", shorttxid(responseMsg.Txid), pb.ChaincodeMessage_ERROR)
		return handler.createResponse(ERROR, responseMsg.Payload)
	}

	// Incorrect chaincode message received
	chaincodeLogger.Errorf("[%s] Incorrect chaincode message %s received. Expecting %s or %s", shorttxid(responseMsg.Txid), responseMsg.Type, pb.ChaincodeMessage_RESPONSE, pb.ChaincodeMessage_ERROR)
	return handler.createResponse(ERROR, []byte(fmt.Sprintf("[%s] Incorrect chaincode message %s received. Expecting %s or %s", shorttxid(responseMsg.Txid), responseMsg.Type, pb.ChaincodeMessage_RESPONSE, pb.ChaincodeMessage_ERROR)))
}

我們來說下上面的步驟:

  1. 序列化查詢參數
  2. 使用channelId+ txid創建了一個txCtxID通道(這裏的通道指的是go裏的通道,用於消息的發送和接收,不是fabric裏的,不要混淆。)
  3. 構造INVOKE_CHAINCODE類型的消息
  4. sendReceive(msg *pb.ChaincodeMessage, c chan pb.ChaincodeMessage) 通過grpc發送invokeChaincode(包括查詢參數,channelID和交易ID)消息直到響應正確的消息。
    1. serialSendAsync(msg, errc)
    2. serialSend(msg *pb.ChaincodeMessage)
  5. 處理響應,如果接收到ChaincodeMessage_RESPONSE和ChaincodeMessage_COMPLETED類型的消息,說明InvokeChaincode成功,否則失敗。
  6. 刪除txCtxID

總結:InvokeChaincode本質上是構造了一個txCtxID,然後向orderer節點發送消息,最後把消息寫入txCtxID,返回即可。

3. 跨鏈的實現

前面已經提到跨鏈的方案:

  1. Chaincode1:UserA向UserPub轉移10元錢,UserPub把這筆錢標記爲已鎖定:
  2. Chaincode2:通過invokeChaincode查詢UserPub是否已經鎖定該筆錢。未鎖定,則終止該次跨鏈,並把資產轉回UserA。否則執行3
  3. Chaincode2:UserPub向UserB轉移10元錢,同時UserPub把這筆錢標記爲已轉移(注:該筆錢不可退回UserA。)
  4. 跨鏈完成

其本質是通過一個公用賬戶來做到的,通過invokeChaincode來保證金額確實被鎖定的。這裏面其實是有很大的問題,我們需要侵入別人的代碼,這裏就很煩,很不友好。

下面我們來看看其實現:

Chaincode1:

在初始化函數中,我們定義了兩個用戶A和UserPub,以及userPubStatus。

然後我們調用Chaincode1從A向UserPub轉10元錢,同時把userPubStatus置位0。

然後我們在Chaincode2調用invokeChaincode查詢UserPub是否已經鎖定該筆錢,即若userPubStatus爲0,則已鎖定。

然後在Chaincode2中從UserPub向UserB(UserB原來有200元錢)轉10元錢。同時在Chaincode1中把UserPub設置爲1。

下面是UserB轉賬前和轉賬後的餘額:

4. 總結

在這次方案的研究中,還是踩了很多的坑的,現總結如下:

  1. 對待一個陌生的東西,一定要先看官方文檔,然後寫個簡單的demo進行驗證。不要急着先幹活。根據驗證的結果在決定下面怎麼辦?

跨鏈在實際的業務中還是需要的,雖然無法通過chaincode來實現,但是還是要想其他辦法的。

跨鏈的實現是很複雜的,中間人這個方案需要很多的前置條件的,現在列出來:

跨鏈前提 原因
兩條鏈需要有一個共同的物理節點 有相同的物理節點纔可以查詢到數據
需要有一箇中間賬戶 中間賬戶保證不會出現雙花問題
中間賬戶必須是相同的CA簽發的 相同CA可以保證同一個用戶
必須侵入跨鏈雙方的鏈碼 轉賬的邏輯是在雙方鏈碼實現的
雙方認可的轉賬流程 保證
發佈了20 篇原創文章 · 獲贊 19 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章