Hyperledger Fabric从源码分析交易
在上一章Hyperledger Fabric从源码分析区块结构中提到了区块的概念,并从源码角度对区块的结构进行了剖析,知道一个简单的区块包含了下面几个部分
- BlockHeader, 区块头
- BlockData,区块数据
- BlockMetaData,区块元数据
上篇文章中已经对区块头,区块数据,区块元数据各个字段的内容进行了分析。我们知道BlockData字段是一个切片,存储的是一条条交易,但是并没有对交易具体的结构进行剖析。那么这篇文章就来分析一下一条交易里面会有哪些东西。
1. 先猜想一下一条交易会有哪些东西
老规矩,先猜想一下一条交易会有哪些数据信息。下面猜想的肯定有对的,也有不对的,这也是一个学习的思路。
先来看下一个交易流程图,方便我们做猜想,我在之前的文章 Hyperledger Fabric的网络拓扑图与交易流程中画了一张图来演示Fabric交易的整体流程,现在再把这张图拿出来看一下。
对着交易流程,一步步分析。
首先客户端提交一个交易提案给背书节点,好那么一个交易应该会有客户端,也就是交易发起方的一些基础信息,可能会包括客户端证书签名等等信息。
之后交易到达背书节点,背书节点会模拟执行交易并进行签名。好那么这条交易也应该会有一个背书节点的签名信息。
客户端拿到模拟执行结果以后向排序节点提交交易,但是排序节点是分通道的,到底是发送给哪个通道呢,所以交易应该也会带一个具体发送到哪个通道的信息
再加上交易本身的一些数据,可能包括交易类型,交易双方地址,交易数据(比如A向B转账10),这些可以称做交易的基础数据。
之后交易就被排序节点打包成区块了,因此我们对交易内容的分析到这里就可以了。上面的说的都是一些我还没看任何资料的一些理解,下面看看源码看看一条交易里面到底有什么。
2. 源码的交易结构
还是来看看之前分析区块时的一个函数NewBlock
func NewBlock(env []*common.Envelope, blockNum uint64, previousHash []byte) *common.Block {
block := common.NewBlock(blockNum, previousHash)
for i := 0; i < len(env); i++ {
// 一条交易数据
txEnvBytes, _ := proto.Marshal(env[i])
// 在创建区块的时候就写入交易数据
block.Data.Data = append(block.Data.Data, txEnvBytes)
}
block.Header.DataHash = block.Data.Hash()
utils.InitBlockMetadata(block)
block.Metadata.Metadata[common.BlockMetadataIndex_TRANSACTIONS_FILTER] = lutils.NewTxValidationFlagsSetValue(len(env), pb.TxValidationCode_VALID)
return block
}
可以看到传参的时候了一个Envelope类型的数据,这是一个切片,然后又将该切片的数据序列化以后复制给了BlockData,因为BlockData中存储的是交易数据,因此可以推断Envelope类型就是交易的结构原型。下面看看这个结构
// Envelope wraps a Payload with a signature so that the message may be authenticated
type Envelope struct {
// A marshaled Payload
Payload []byte
// A signature by the creator specified in the Payload header
Signature []byte
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
Envelope直译是信封的意思,可以形象地理解,一条交易是一个信封,在排序节点排序的时候不会去拆开信封看,也就是说排序节点不会关心交易的数据,它的作用就是排序(共识)。而当交易被打包成区块分发的各个记账节点时,记账节点就会去拆开这个信封,也就是会去关心交易的数据,因为它要记账,另外还有做验证这之类的操作。所以Fabric中用Envelope这个单词来表示一个交易。它内部有两个字段
- Payload,直译过来就是有效载荷,抽象一下,payload 可以理解为一系列信息中最为关键的信息,也就是说交易的关键信息都存储在Payload当中,它是一个序列化的字节数组
- Signature,签名信息,根据注释信息可以知道,这个签名就是Payload header中指定的交易的创建者的签名,应该就是之前分析时说的客户端的签名
PayLoad字段解析
下面的重要任务就是对Payload这个字段进行分析了,因此要对Payload序列化之前的结构进行分析。我在源码中找到了一处关于Payload的赋值的地方
updateResult := &cb.Envelope{
Payload: utils.MarshalOrPanic(&cb.Payload{Header: &cb.Header{
ChannelHeader: utils.MarshalOrPanic(&cb.ChannelHeader{
Type: int32(cb.HeaderType_CONFIG),
ChannelId: chainID,
}),
SignatureHeader: utils.MarshalOrPanic(&cb.SignatureHeader{
Creator: signerSerialized,
Nonce: utils.CreateNonceOrPanic(),
}),
},
Data: utils.MarshalOrPanic(&cb.ConfigEnvelope{
LastUpdate: chCrtEnv,
}),
}),
}
上面这条赋值语句不仅让我们找到了Payload字段序列化之前的结构体Payload,在Payload字段内部的一些字段序列化之前的信息也可以找到,这样就一锅端了很好,下面就一起来看下这几个结构体。
// Payload is the message contents (and header to allow for signing)
type Payload struct {
// Header is included to provide identity and prevent replay
Header *Header
// Data, the encoding of which is defined by the type in the header
Data []byte
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
Header字段
首先看下Payload结构体,它有两个字段
- Header,Payload的头部字段信息
- Data,Payload的具体数据,说到底,它就是交易transaction的序列化信息
看下Header字段包含了哪些信息,包括它各字段的序列化对象
type Header struct {
// 顾名思义,交易通道的Header
ChannelHeader []byte
// 顾名思义,交易签名的Header
SignatureHeader []byte
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
// Header is a generic replay prevention and identity message to include in a signed payload
type ChannelHeader struct {
// HeaderType,之前说的Payload中Data字段会与这个Type有关
// 它包含下面几个类型,具体每个Heaeder类型是做什么的这里暂时不说了
// const (
// HeaderType_MESSAGE HeaderType = 0
// HeaderType_CONFIG HeaderType = 1
// HeaderType_CONFIG_UPDATE HeaderType = 2
// HeaderType_ENDORSER_TRANSACTION HeaderType = 3
// HeaderType_ORDERER_TRANSACTION HeaderType = 4
// HeaderType_DELIVER_SEEK_INFO HeaderType = 5
// HeaderType_CHAINCODE_PACKAGE HeaderType = 6
// HeaderType_PEER_ADMIN_OPERATION HeaderType = 8
// HeaderType_TOKEN_TRANSACTION HeaderType = 9
//)
Type int32
// Version表示消息协议的版本
// 在创世区块中被这么赋值 msgVersion = int32(1)
Version int32
// Timestamp表示这条消息创建时的本地时间
Timestamp *timestamp.Timestamp
// ChannelId表示这条消息绑定的通道的ID
ChannelId string
// TxID表示交易的唯一标识符,唯一ID
TxId string
// Epoch直译表示新纪元,epoch表示生成此Header的纪元,他是根据Block height定义的
Epoch uint64
// Extension是根据Type字段可以附加的扩展部分
Extension []byte
// TlsCertHash表示客户端TLS证书的哈希,如果使用的是mutual TLS
TlsCertHash []byte
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
type SignatureHeader struct {
// Creator 表示消息的创建者,他是一个序列化的身份信息
Creator []byte
// Nonce是只使用一次的随机数字,可用于检测replay attacks
// Creator和Nonce两个字段可以用来生成一个TxID
Nonce []byte
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
针对上面的描述再做一个总结
ChannelHeader(通道Header)
- Type,Header的type
- Version,消息协议的版本
- Timestamp,消息创建的时间戳
- ChannelID,消息绑定的通道的ID
- TxID,消息的唯一标识符
- Epoch,表示此Header的纪元,根据Block height定义的
- Extension,根据Type字段可以附加的扩展部分
SignatureHeader(签名Header)
- Creator,表示消息的创建者,他是一个序列化的身份信息
- Nonce,他是一个随机数,可用于检测replay attacks,Creator和Nonce两个字段可以用来生成一个TxID
Header字段到这里就分析完了
Data字段
Data具体的反序列化对象,根据ChannelHeader中的Type不同而不同。
Data的序列化与反序列化过程,不同的交易类型会导致Data的元数据不同,即当Payload.Data赋值的时候,他可能是不同的结构体对象序列化之后的字节切片。
但是在验证交易需要获取交易信息的时候,又被反序列化成了一个统一的结构对象——Transaction,找一处调用看一下
Transaction
// Transaction
tx, err := utils.GetTransaction(payload.Data)
// GetTransaction Get Transaction from bytes
func GetTransaction(txBytes []byte) (*peer.Transaction, error) {
tx := &peer.Transaction{}
err := proto.Unmarshal(txBytes, tx)
return tx, errors.Wrap(err, "error unmarshaling Transaction")
}
可以看到获取是从Payload.Data中反序列化成了一个Transaction对象,因此我们分析Data的时候,就按照Transaction对象分析就可以了。
- Transaction是要被送给排序节点的最终结果
- Transaction包括一个或多个TransactionAction,每个TransactionAction都将一个提案绑定到潜在的多个actions
- Transaction是原子性的,这意味着要么提交Transaction里的所有actions,或者不提交
- 意当一个Transaction包含一个以上的Header时,那么每个Header.Creator字段必须一样
- 一个client可以*发布多个独立的提案,每个提案都包含他们的Header和要求的payload(ChaincodeProposalPayload)
- 每个提案都被独立背书,生成一个aciton(ProposalResponsePayload),每个背书者都有签名
- 任意数量的独立提案(以及他们的actions)可能包含在一个Transaction中以确保他们是原子的
type Transaction struct {
// Actions是TransactionAction的数组
// 为了每个Transaction容纳多个acitons,必须要是数组
Actions []*TransactionAction
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
那么下面来看看TransactionAction结构体
TransactionAction
// TransactionAction 绑定一个提案到它的aciton.
// header中的type字段决定了应用于账本的action的type
type TransactionAction struct {
// 交易提案action的Header,Proposal的header
Header []byte
// 交易提案action的主要信息
// 当type是chaincode时,它是ChaincodeActionPayload的序列化
Payload []byte
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
TransactionAction.Payload
字段就是接下来分析的重点,该字段根据header中的type字段而定义,当type是CHAINCODE时,它是ChaincodeActionPayload
的序列化,那么来看下ChaincodeActionPayload
结构体
ChaincodeActionPayload
// ChaincodeActionPayload是当Header的type设置为CHAINCODE时用于赋值给TransactionAction的Payload的
type ChaincodeActionPayload struct {
// 这个字段是 ChaincodeProposalPayload 类型的序列化信息
ChaincodeProposalPayload []byte
// 应用于账本的actions的列表
Action *ChaincodeEndorsedAction
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
ChaincodeActionPayload
两个模块组成
-
ChaincodeProposalPayload,这个字段是
ChaincodeProposalPayload
类型的序列化信息,它包含了原始调用函数的参数信息 -
Action,这个字段表示的actions的列表 ,它是一个
ChaincodeEndorsedAction
类型
先来看看ChaincodeProposalPayload
结构体
ChaincodeProposalPayload
// ChaincodeProposalPayload是提案的具体信息,当Header的类型是CHAINCODE时
// 它包含了这次调用的参数
type ChaincodeProposalPayload struct {
// 包含了这次调用的参数
Input []byte
// TransientMap包含一些数据(例如密码材料),可以用来实现某种形式的应用级机密性
// 这个字段的内容应该总是从交易中被省略,从分类账中被排除。
TransientMap map[string][]byte
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
ChaincodeProposalPayload
是当Header的type字段是CHAINCODE时,提案的主要信息,主要包含了两个字段
- Input,包含了这次链码调用的一些参数
- TransientMap,包含一些数据(例如密码材料),可以用来实现某种形式的应用级机密性,这个字段的内容应该总是从交易中被省略,从分类账中被排除。
再来看看ChaincodeEndorsedAction
结构体
ChaincodeEndorsedAction
// ChaincodeEndorsedAction 包含了一个指定提案的背书信息
type ChaincodeEndorsedAction struct {
// 提案响应信息
ProposalResponsePayload []byte
// 提案的背书,基本上是背书者在proposalResponsePayload上的签名
Endorsements []*Endorsement
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
ChaincodeEndorsedAction
包含了一个指定提案的背书信息,主要包含了两个字段
-
ProposalResponsePayload,提案的响应信息,它是
ProposalResponsePayload
的序列化 -
Endorsements, 提案的背书,是一些背书人在
ProposalResponsePayload
上的签名
先来看看ProposalResponsePayload
结构体
ProposalResponsePayload
type ProposalResponsePayload struct {
// 提案相应的哈希
ProposalHash []byte
// 根据Type字段可以附加的扩展部分
// 对于CHAINCODE而言,它是ChaincodeAction的序列化信息
Extension []byte
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
// ChaincodeAction包含了链码执行生成的events的actions
type ChaincodeAction struct {
// 包含了链码执行的读写集
Results []byte
// 包含了链码调用生成的events
Events []byte
// 包含了链码调用生成的Response
Response *Response
// 链码ID
ChaincodeId *ChaincodeID
// 包含了链码调用生成的token expectation
TokenExpectation *token.TokenExpectation
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}
最后看下Endorsement
结构体,这主要是一些背书人的身份信息及签名。
Endorsement
type Endorsement struct {
// 背书者本身的身份信息
Endorser []byte
// 背书签名
Signature []byte
XXX_NoUnkeyedLiteral struct{}
XXX_unrecognized []byte
XXX_sizecache int32
}