1. 程式人生 > >深度探索Hyperledger技術與應用之超級賬本的典型交易流程

深度探索Hyperledger技術與應用之超級賬本的典型交易流程

image

1

典型交易流程

下圖所示為Hyperledger Fabric 1.0典型的交易流程圖。

image

從上一節的網路節點架構中,我們已經瞭解到基於Hyperledger Fabric 1.0的區塊鏈應用中涉及幾個節點角色:應用程式、背書節點、排序服務節點和主節點。在圖3-4中,假定各節點已經提前頒發好證書,且已正常啟動,並加入已經建立好的通道。後面的步驟介紹在已經例項化了的鏈碼通道上從發起一個呼叫交易到最終記賬的全過程。

1、建立交易提案併發送給背書節點

使用應用程式構造交易提案,SignedProposal的結構如下所示:

SignedProposal: {

    ProposalBytes(Proposal)
: { Header: { ChannelHeader: { Type: "HeaderType_ENDORSER_TRANSACTION", TxId: TxId, Timestamp: Timestamp, ChannelId: ChannelId, Extension(ChaincodeHeaderExtension): { PayloadVisibility
: PayloadVisibility, ChaincodeId: { Path: Path, Name: Name, Version: Version } }, Epoch: Epoch }, SignatureHeader: { Creator
: Creator, Nonce: Nonce
} }, Payload: { ChaincodeProposalPayload: { Input(ChaincodeInvocationSpec): { ChaincodeSpec: { Type: Type, ChaincodeId: { Name: Name }, Input(ChaincodeInput): { Args: [] } } }, TransientMap: TransientMap } } }, Signature: Signature }

我們來看看上面的結構,SignedProposal是封裝了Proposal的結構,添加了呼叫者的簽名信息。背書節點會根據簽名信息驗證其是否是一個有效的訊息。Proposal由兩個部分組成:訊息頭和訊息結構。訊息結構詳細的解釋參考後面的章節。這裡簡單講一下訊息頭。

訊息頭(Header)也包含兩項內容。

通道頭(ChannelHeader):通道頭包含了與通道和鏈碼呼叫相關的資訊,比如在哪個通道上呼叫哪個版本的鏈碼。TxId是應用程式本地生成的交易號,跟呼叫者的身份證書相關,可以避免交易號的衝突,背書節點和記賬節點都會校驗是否存在重複交易。

簽名頭(SignatureHeader):簽名頭包含了呼叫者的身份證書和一個隨機數,用於訊息的有效性校驗。

應用程式構造好交易提案請求後,選擇背書節點執行並進行背書籤名。背書節點是鏈碼背書策略裡指定的節點。有一些背書節點是離線的,其他的背書節點可以拒絕對交易進行背書,也可以不背書。應用程式可以嘗試使用其他可用的背書節點來滿足策略。應用程式以何種順序給背書節點發送背書請求是沒有關係的,正常情況下背書節點執行後的結果是一致的,只有背書節點對結果的簽名不一樣。

2、背書節點模擬交易並生成背書籤名

背書節點在收到交易提案後會進行一些驗證,包括:

  • 交易提案的格式是否正確;

  • 交易是否提交過(重複攻擊保護);

  • 交易簽名有效(通過 MSP);

  • 交易提案的提交者在當前通道上是否已授權有寫許可權。

驗證通過後,背書節點會根據當前賬本資料模擬執行鏈碼中的業務邏輯並生成讀寫集(RwSet),其中包含響應值、讀寫集等。在模擬執行時賬本資料不會更新。而後背書節點對這些讀寫集進行簽名成為提案響應(Proposal Response),然後返回給應用程式。

ProposalResponse的結構如下:

ProposalResponse: {

    Version: Version,

    Timestamp: Timestamp,

    Response: {

        Status: Status,

        Message: Message,

        Payload: Payload

    },

    Payload(ProposalResponsePayload): {

        ProposalHash: ProposalHash,

        Extension(ChaincodeAction): {

            Results(TxRwSet): {

                NsRwSets(NsRwSet): [

                    NameSpace: NameSpace,

                    KvRwSet: {

                        Reads(KVRead): [

                            Key: Key,

                            Version: {

                                BlockNum: BlockNum,

                                TxNum: TxNum

                            }

                        ],

                        RangeQueriesInfo(RangeQueryInfo): [

                            StartKey: StartKey,

                            EndKey: EndKey,

                            ItrExhausted: ItrExhausted,

                            ReadsInfo: ReadsInfo

                        ],

                        Writes(KVWrite): [

                            Key: Key,

                            IsDelete: IsDelete,

                            Value: Value

                        ]

                    }

                ]

            },

            Events(ChaincodeEvent): {

                ChaincodeId: ChaincodeId,

                TxId: TxId,

                EventName: EventName,

                Payload: Payload

            }

            Response: {

                Status: Status,

                Message: Message,

                Payload: Payload

            },

            ChaincodeId: ChaincodeId

        }

    },

    Endorsement: {

        Endorser: Endorser,

        Signature: Signature

    }

}

返回的ProposalResponse中包含了讀寫集、背書節點簽名以及通道名稱等資訊。

3、收集交易的背書

應用程式收到ProposalResponse後會對背書節點簽名進行驗證,所有節點接收到任何訊息後都是需要先驗證訊息合法性的。如果鏈碼只進行賬本查詢,應用程式會檢查查詢響應,但不會將交易提交給排序服務節點。如果鏈碼對賬本進行Invoke操作,則須提交交易給排序服務進行賬本更新,應用程式會在提交交易前判斷背書策略是否滿足。如果應用程式沒有收集到足夠的背書就提交交易了,記賬節點在提交驗證階段會發現交易不能滿足背書策略,標記為無效交易。

如何選擇背書節點呢?目前fabric-sdk-go預設的實現是把配置檔案選項channels.mychannel.peers(其中的mychannel需要替換成實際的通道名稱)裡的節點全部新增為背書節點,需要等待所有背書節點的背書籤名。應用程式等待每個背書節點執行的超時時間是通過配置檔案選項client.peer.timeout.connection設定的,配置檔案的示例給出的是3秒,根據實際情況調整,如果沒有設定就是5秒的預設值。

4、構造交易請求併發送給排序服務節點

應用程式接收到所有的背書節點簽名後,根據背書籤名呼叫SDK生成交易,廣播給排序服務節點。生成交易的過程比較簡單,確認所有的背書節點的執行結果完全一致,再將交易提案、提案響應和背書籤名打包生成交易。交易的結構如下:

Envelope: {

    Payload: {

        Header: {

            ChannelHeader: {

                Type: "HeaderType_ENDORSER_TRANSACTION",

                TxId: TxId,

                Timestamp: Timestamp,

                ChannelId: ChannelId,

                Extension(ChaincodeHeaderExtension): {

                    PayloadVisibility: PayloadVisibility,

                    ChaincodeId: {

                        Path: Path,

                        Name: Name,

                        Version: Version

                    }

                },

                Epoch: Epoch

            },

            SignatureHeader: {

                Creator: Creator,

                Nonce: Nonce

            }

        },

        Data(Transaction): {

            TransactionAction: [

                Header(SignatureHeader): {

                    Creator: Creator,

                    Nonce: Nonce

                },

                Payload(ChaincodeActionPayload): {

                    ChaincodeProposalPayload: {

                        Input(ChaincodeInvocationSpec): {

                            ChaincodeSpec: {

                                Type: Type,

                                ChaincodeId: {

                                    Name: Name

                                },

                                Input(ChaincodeInput): {

                                    Args: []

                                }

                            }

                        },

                        TransientMap: nil

                    },

                    Action(ChaincodeEndorsedAction): {

                        Payload(ProposalResponsePayload): {

                            ProposalHash: ProposalHash,

                            Extension(ChaincodeAction): {

                                Results(TxRwSet): {

                                    NsRwSets(NsRwSet): [

                                        NameSpace: NameSpace,

                                        KvRwSet: {

                                            Reads(KVRead): [

                                                Key: Key,

                                                Version: {

                                                    BlockNum: BlockNum,

                                                    TxNum: TxNum

                                                }

                                            ],

                                            RangeQueriesInfo(RangeQueryInfo): [

                                                StartKey: StartKey,

                                                EndKey: EndKey,

                                                ItrExhausted: ItrExhausted,

                                                ReadsInfo: ReadsInfo

                                            ],

                                            Writes(KVWrite): [

                                                Key: Key,

                                                IsDelete: IsDelete,

                                                Value: Value

                                            ]

                                        }

                                    ]

                                },

                                Events(ChaincodeEvent): {

                                    ChaincodeId: ChaincodeId,

                                    TxId: TxId,

                                    EventName: EventName,

                                    Payload: Payload

                                }

                                Response: {

                                    Status: Status,

                                    Message: Message,

                                    Payload: Payload

                                },

                                ChaincodeId: ChaincodeId

                            }

                        },

                        Endorsement: [

                            Endorser: Endorser,

                            Signature: Signature

                        ]

                    }

                }

            ]

        }

    },

    Signature: Signature

}

我們來看交易信封的幾個對應關係:

  • Envelope.Payload.Header同交易提案SignedProposal.Proposal.Header;

  • Envelope.Payload.Data.TransactionAction.Header是交易提案的提交者的身份資訊,同SignedProposal.Proposal.Header.SignatureHeader和Envelope.Payload.Header.SignatureHeader是冗餘的;

  • Envelope.Payload.Data.TransactionAction.Payload.ChaincodeProposalPayload同交易提案的SignedProposal.Proposal.Payload.ChaincodeProposalPayload,唯一不同的是,TransientMap強制設定為nil,目的是避免在區塊中出現一些敏感資訊;

  • Envelope.Payload.Data.TransactionAction.Payload.Action.Payload結構,其實和Proposal

  • Response.Payload結構完全一樣;

  • Envelope.Payload.Data.TransactionAction.Payload.Action.Endorsement變成了陣列,代表多個背書節點的背書籤名。

整個信封Envelope的Signature是交易提交者對整個Envelope.Payload的簽名。應用程式可以把生成的交易信封內容傳送給任意選擇的幾個排序服務節點。

5、排序服務節點以對交易進行排序並生成區塊

排序服務不讀取交易的內容,如果在生成交易信封內容的時候偽造了交易模擬執行的結果,排序服務節點也不會發現,但會在最終的交易驗證階段校驗出來並標記為無效交易。排序服務要做得很簡單,先是接收網路中所有通道發出的交易資訊,讀取交易信封的Envelope.Payload.Header.ChannelHeader.ChannelId以獲取通道名稱,按各個通道上交易的接收時間順序對交易資訊進行排序,生成區塊。

6、排序服務節點以廣播給組織的主節點

排序服務節點生成區塊以後會廣播給通道上不同組織的主節點。

7、記賬節點驗證區塊內容並寫入區塊

背書節點是動態角色,只要參與交易的背書就是背書節點,哪些交易選擇哪些節點作為背書節點是由應用程式選擇的,這需要滿足背書策略才能生效。所有的背書節點都屬於記賬節點。所有的Peer節點都是記賬節點,記錄的是節點已加入通道的賬本資料。記賬節點接收到的是排序服務節點生成的區塊,驗證區塊交易的有效性,提交到本地賬本後再產生一個生成區塊的事件,監聽區塊事件的應用程式可以進行後續的處理。

如果接收到的區塊是配置區塊,則會更新快取的配置資訊。記賬節點的處理流程如圖所示。

image

交易資料的驗證

區塊資料的驗證是以交易驗證為單位的,每次對區塊進行驗證時都會生成一個交易號的點陣圖TxValidationFlags,它記錄每個交易號的交易驗證狀態,只有狀態為TxValidationCode_VALID才是有效的。點陣圖也會寫入到區塊的元資料BlockMetadataIndex_TRANSACTIONS_FILTER中。交易驗證的時候會檢查以下內容:

  • 是否為合法的交易:交易格式是否正確,是否有合法的簽名,交易內容是否被篡改;

  • 記賬節點是否加入了這個通道。

基本的驗證通過以後會提交給VSCC進行背書策略的驗證。

記賬節點與VSCC

鏈碼的交易是隔離的,每個交易的模擬執行結果讀寫集TxRwSet都包含了交易所屬的鏈碼。為了避免錯誤地更新鏈碼交易資料,在交易提交給系統鏈碼VSCC驗證交易內容之前,還會對鏈碼進行校驗。下面這些交易都是非法的:

  • 鏈碼的名稱或者版本為空;

  • 交易訊息頭裡的鏈碼名稱Envelope.Payload.Header.ChannelHeader.Extension.ChaincodeId. Name和交易資料裡的鏈碼名稱Envelope.Payload.Data.TransactionAction.Payload.ChaincodeProposalPayload.Input.ChaincodeSpec.ChaincodeId.Name不一致;

  • 鏈碼更新當前鏈碼資料時,生成讀寫集的鏈碼版本不是LSCC記錄的最新版本;

  • 應用程式鏈碼更新了LSCC(生命週期管理系統鏈碼)的資料;

  • 應用程式鏈碼更新了不可被外部呼叫的系統鏈碼的資料;

  • 應用程式鏈碼更新了不可被其他鏈碼呼叫的系統鏈碼的資料;

  • 呼叫了不可被外部呼叫的系統鏈碼。

基於狀態資料的驗證和MVCC檢查

交易通過VSCC檢查以後,就進入記賬流程。kvledger還會對讀寫集TxRwSet進行MVCC(Multi-Version Concurrency Control)檢查。

kvledger實現的是基於鍵值對(key-value)的狀態資料模型。對狀態資料的鍵有3種操作:

  • 讀狀態資料;

  • 寫狀態資料;

  • 刪除狀態資料。

對狀態資料的讀操作有兩種形式:

  • 基於單一鍵的讀取;

  • 基於鍵範圍的讀取。

MVCC檢查只對讀資料進行校驗,基本邏輯是對模擬執行時狀態資料的版本和提交交易時狀態資料的版本進行比較。如果資料版本發生變化或者某個鍵的範圍資料發生變化,就說明這段時間之內有別的交易改變了狀態資料,當前交易基於原有狀態的處理就是有問題的。由於交易提交是並行的,所以在交易未打包生成區塊之前,並不能確定最終的執行順序。如果交易執行的順序存在依賴,在MVCC檢查的時候就會出現依賴的狀態發生了變化,實際上是資料出現了衝突。圖所示為基於狀態的資料驗證的流程圖。

image

寫集合本身包含了寫和刪除的資料,有一個狀態位標識是否刪除資料。為了提升效率,狀態資料庫的提交是批處理的,整個區塊交易的狀態資料同時提交,這也保證了整個區塊的狀態資料要麼都提交成功,要麼都提交失敗。這時只會出現記錄的賬本資料和狀態資料庫不一致,不會出現區塊的狀態資料不一致的情況。當賬本資料和狀態資料庫不一致時,可以通過狀態資料庫的檢查點來標記。

無效交易的處理

偽造的交易會導致無效交易,正常的交易也可能出現無效交易。MVCC檢查的是背書節點在模擬執行的時候,環境是否和記賬節點提交交易時的環境一致,這裡的環境是指狀態資料庫裡資料的三元組(key、value、version)是否完全一致。如果正常提交的交易在這個過程中涉及的資料發生了變化,那麼也會出現檢查失敗從而導致無效交易。在這種情況下,需要在上層的應用程式有一些補償措施,比如調整交易打包的配置,重新提交失敗的交易等。

在目前版本的實現中,無效交易也會保留在區塊中,可以通過區塊記錄的元資料確定哪些是無效交易。無效交易的讀寫集不會提交到狀態資料庫中,不會導致狀態資料庫發生變化,只是會佔用區塊的大小,佔用記賬節點的硬碟空間。後續的版本會實現賬本的精簡,過濾掉無效交易。

8、在組織內部同步最新的區塊

下篇預告:深度探索Hyperledger技術與應用之超級賬本的策略管理

image

深度探索區塊鏈

Hyperledger技術與應用

區塊鏈

張增駿,董寧,朱軒彤,陳劍雄  著

本書由超級賬本執行董事Brian Behlendorf領銜推薦,區塊鏈一線落地實踐團隊、Hyperleger會員智鏈骨幹團對撰寫。深入講解Hyperledger Fabric 1.0的架構、執行邏輯、核心功能實現、從零部署,並以票據案例為例,講解具體開發實踐,穿插開發所需的最佳實踐和遇到的問題解決。

機械工業

出版社

image

華章科技是機械出版社的旗下品牌,出版了“電腦科學叢書”等近30個經典套系,在各個細分領域均處於領導地位,其中《Java程式設計思想》、《演算法導論》、《編譯原理》、《資料探勘:概念與技術》、《深入理解計算機系統》、《深入理解Java虛擬機器》等著作猶如計算機圖書領域的璀璨明珠,長銷不衰!

image

HiBlock秉承開放、協作、透明、連結、分享的價值觀,致力打造區塊鏈開發者社群。專注於在開發者中推廣區塊鏈,幫助開發者真正掌握區塊鏈技術和應用。

本文內容節選自《深度探索區塊鏈:Hyperledger技術與應用》一書的第3章《超級賬本的系統架構》。

本書作者:張增駿,董寧,朱軒彤,陳劍雄

感謝機械工業出版社華章分社的支援和分享。

以下是我們的社群介紹,歡迎各種合作、交流、學習:)

image