目錄html
今天公司讓我整理一個基於fabric的跨鏈的方案,以前沒怎麼接觸過跨鏈,在這裏記錄下本身的思路吧。git
首先,先明白幾個概念。什麼是跨鏈?個人理解是跨鏈是跨channel。下面詳細說下個人理由:github
跨鏈咱們既能夠在上層來作,也能夠在chaincode層來作。通過查找我發現了一個InvokeChaincode方法,看着不錯,看上去是用來調用其餘的chaincode的。docker
因此我設計以下的跨鏈方案:api
簡單描述下:Org1中的peer1和ORG3中的peer3加入channel1,而且安裝Chaincode1,Org2中的peer2 和ORG3中的peer3加入channel2,而且安裝Chaincode2。
peer3這個節點是能夠跨鏈的關鍵所在,由於該節點同時擁有兩個通道的數據。網絡
先整個簡易版的跨鏈流程:app
事情到這裏,並無完,上面的操做不是一個原子操做,因此咱們必需要考慮事務性,若是中間步驟出錯,咱們要將整個過程進行回滾,而且這是在分佈式的環境下完成的,哎,真的讓人頭大。分佈式
工欲善其事必先利其器,下面咱們來搭建跨鏈所需的環境ide
在開始以前,咱們須要相應的搭建相應的開發環境,我是在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命令進行查看。
咱們須要修改configtx.yaml文件和generateArtifacts.sh文件。
咱們使用的主要工具是configtxgen工具。目的是生成系統通道的創世區塊,兩個應用通道channel1和channel2的配置交易文件,每一個channel的每一個組織都要生成錨節點配置更新交易文件。生成後的文件以下所示:
咱們首先可使用docker-comppose-e2e來測試下網絡的聯通是否正常。
docker-compose -f docker-compose-e2e.yaml
看看網絡是不是正常的 ,不正常的要及時調整。
接下來,咱們修改docker-compose-cli.yaml,咱們使用fabric提供的fabric-tools鏡像來建立cli容器來代替SDK。
這裏主要使用的是script.sh來建立網絡,啓動orderer節點和peer節點。
咱們建立channel1,channel2,把各個節點分別加入channel,更新錨節點,安裝鏈碼,實例化鏈碼。
上面的操做所有沒有錯誤後,咱們就搭建好了跨鏈的環境了,這裏在逼逼一句,咱們建立了兩個通道,每一個通道兩個組織,其中Org3是其交集。下面能夠正式的進行跨鏈了。
其實在前面的操做中,並非一路順風的,你們能夠看到,須要修改的文件其實仍是蠻多的,有一個地方出錯,網絡就啓動不了,建議你們分步進行運行,一步一步的解決問題,好比說,我在configtx.yaml文件中,ORG3的MSPTYPE指定成了idemix類型的,致使後面不管如何也驗證不過,通道沒法建立成功。
簡單說下idemix,這個玩意是fabric v1.3 引入的一個新的特性,是用來用戶作隱私保護的,基於零知識證實的知識,這裏不在詳述,感興趣的能夠參考:
fabric關於idemix的描述
找到fabric提供了這麼一個函數的文檔,咱們先來看看。
// 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來驗證下。
下面我簡單搭建一個測試網絡來進行驗證,仍是兩個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,但前提必須是有共同的物理節點。
下面的內容不是必須看的,咱們來深刻進去看看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))) }
咱們來講下上面的步驟:
總結:InvokeChaincode本質上是構造了一個txCtxID,而後向orderer節點發送消息,最後把消息寫入txCtxID,返回便可。
前面已經提到跨鏈的方案:
其本質是經過一個公用帳戶來作到的,經過invokeChaincode來保證金額確實被鎖定的。這裏面實際上是有很大的問題,咱們須要侵入別人的代碼,這裏就很煩,很不友好。
在此次方案的研究中,仍是踩了不少的坑的,現總結以下:
跨鏈在實際的業務中仍是須要的,雖然沒法經過chaincode來實現,可是仍是要想其餘辦法的。