死磕hyperledger fabric源码|Endorser节点背书服务
文章及代码:https://github.com/blockchainGuide/
分支:v1.1.0
背书概述
Endorser背书节点提供ProcessProposal()
服务接口用于接收与处理签名提案消息的请求,启动用户链码容器,执行调用链码,并对模拟执行结果进行签名背书,。Peer
节点启动时解析core.yaml
文件中的peer.handlers
配置项,并构造认证过滤器列表。如果存在合法类型的认证过滤器,则需要先经过所有认证过滤器调用ProcessProposal
()方法进行验证过滤,例如检查身份证书是否过期,然后再提交给背书服务器的serverEndorser.ProcessProposal
()方法进行处理。 方法功能如下:
1 | func (e *Endorser) ProcessProposal(ctx context.Context, signedProp *pb.SignedProposal) (*pb.ProposalResponse, error) { |
主要做了以下几件事:
- 调用
preProcess()
方法预处理签名提案消息,验证消息合法性 - 调用
simulateProposal()
方法启动链码容器并模拟执行提案,将结果读写集记录到模拟交易器中。 - 调用
endorseProposal()
方法对模拟执行结果进行签名背书,并返回提案响应消息。
下面的内容将会紧紧围绕这几部分来进行分析。
预处理签名提案消息
进入到preProcess
函数:
①: 验证签名提案消息格式与签名的合法性
1 | prop, hdr, hdrExt, err := validation.ValidateProposalMessage(signedProp) |
②: 检查提案消息是否允许外部调用的系统链码
1 | //解析消息通道头部ChannelHeader结构 |
③:检查签名提案消息的唯一性以及是否满足指定通道的访问权限策略
1 | chainID := chdr.ChannelId //获取通道标识号ChannelID,即链chainID |
以上3个部分内容还需要进一步的细化,看接下来的分析。
验证消息格式与签名合法性
①:调用validation.ValidateProposalMessage()
函数,以检查签名提案消息格式与签名的合法性,解析获取提案消息、消息头部及其扩展域。
1.1 校验header
1 | chdr, shdr, err := validateCommonHeader(hdr) |
校验header里面大概做了这几件事:
validateChannelHeader(chdr
)函数检查通道头部chdr
的合法性,其通道头部类型应该属于ENDORSER_TRANSACTION
、CONFIG_UPDATE
、CONFIG
或PEER_RESOURCE_UPDATE
,并且Epoch
字段应该为0;validateSignatureHeader(shdr)
函数检查签名头部shdr
的合法性,随机数Nonce
和消息签名者
Creator
不应该为nil
,并且该对象字节数不为0
1.2 检查消息签名的合法性
1 | err = checkSignatureFromCreator(shdr.Creator, signedProp.Signature, signedProp.ProposalBytes, chdr.ChannelId) |
该方法先获取当前通道的身份反序列化组件mspObj
,解析出该签名头部的签名者creator
,并调用creator.Validate()
方法,验证creator
是否为MSP有效的X.509合法证书。然后,调用creator.Verify()
方法获取哈希方法及消息摘要(哈希值),通过所属MSP
组件的BCCSP
加密安全组件调用id.msp.bccsp.Verify()
方法,验证消息签名的真实性
1.3 验证提案消息头部中的交易ID是否计算正确
1 | err = utils.CheckProposalTxID( |
重新计算消息随机数Nonce
(防止重放攻击)与签名者Creator
组合信息后的哈希值,并且与交易ID进行比较。如果两者匹配相同,则说明交易ID是正确的。
检查是否为允许外部调用的系统链码
1 | if e.s.IsSysCCAndNotInvokableExternal(hdrExt.ChaincodeId.Name) { |
检查签名提案消息的唯一性
preProcess()方法继续检查签名提案消息的唯一性,以防止重放攻击。该方法从提案消息通道头部提取链与交易ID,包括两种情况:
- 如果链ID不是空字符串,则需要检查该交易ID的唯一性,确保之前没有提交过该交易到账本中。即根据交易ID从账本的区块文件以及区块索引数据库获取交易数据与交易验证码,并构造成已处理的交易对象 。如果获取交易数据成功且没有错误,则说明账本中已经保存了指定交易ID的交易数据。因此,当前提案消息属于重复提交,报错返回。否则,就说明该签名提案消息通过了消息唯一性的检查;
- 如果链ID是空字符串,则不需要检查签名提案消息的唯一性与验证通道访问权限策略,只需要通过
ValidateProposalMessage()
函数验证该提案消息的合法性即可。
检查是否满足通道的访问权限策略
首先调用IsSysCC
函数,检查链码是否为系统链码。如果是用户链码,则调用 CheckACL
方法,检查签名提案消息是否满足通道PROPOSE
权限策略要求,以允许提交该消息到指定通道上继续进行处理。CheckACL
方法如下:
1 | func (d *defaultACLProvider) CheckACL(resName string, channelID string, idinfo interface{}) error { |
方法先调用defaultPolicy()
方法,从全局通道资源策略字典cResourcePolicyMap
中获取指定策略名称resources.PROPOSE的默认策略。对于SignedProposal
类型的签名提案消息,CheckACL()
方法调用d.policyChecker.CheckPolicy()
方法,检查该签名提案消息是否满足该通道上的Writers
写权限策略要求。
模拟执行提案
ProcessProposal()方法启动链码容器与初始化链码执行环境,模拟执行合法的签名提案消息,并将模拟执行结果记录在交易模拟器中。其中,对公有数据(包含公共数据与隐私数据哈希值)继续签名背书,并提交给Orderer
节点请求排序出块,同时将隐私数据通过Gossip
消息协议发送到组织内的其他授权节点上。核心函数如下:
1 | func (e *Endorser) simulateProposal(ctx context.Context, chainID string, txid string, signedProp *pb.SignedProposal, prop *pb.Proposal, cid *pb.ChaincodeID, txsim ledger.TxSimulator) (resourcesconfig.ChaincodeDefinition, *pb.Response, []byte, *pb.ChaincodeEvent, error) { |
根据链码类型执行不同实例化策略
首先调用GetChaincodeInvocationSpec
函数,从提案消息中解析提取出链码调用规范对象,然后调用IsSysCC(cid.Name)
方法,依次匹配默认的系统链码名称,以判断当前链码类型是用户链码还是系统链码,分为用户链码和系统链码两种情况检查实例化策略。
①:用户链码
1 | if !e.s.IsSysCC(cid.Name) { // 如果是调用用户链码,则需要保证该链码已经实例化了 |
②:系统链码
1 | else { // === 执行系统链码,如lscc等 |
启动链码容器
1 | res, ccevent, err = e.callChaincode(ctx, chainID, version, txid, signedProp, prop, cis, cid, txsim) |
①:设置context上下文对象中交易模拟器的KV键值对,其中,键为TXSimulatorKey,值为交易模拟器txsim
1 | if txsim != nil { |
②:根据链码名称检查是否为系统链码
1 | scc := e.s.IsSysCC(cid.Name) |
③:执行链码调用
1 | res, ccevent, err = e.s.Execute(ctxt, chainID, cid.Name, version, txid, scc, signedProp, prop, cis) |
④:检查调用链码名称lscc
1 | // 第1个参数为deploy部署或upgrade升级,第2个参数是链ID,第3个是链码部署规范对象 |
启动的真正过程正是在e.s.Execute
中完成的,分析如下:
/core/chaincode/exectransaction.go/Execute()
1 | func Execute(ctxt context.Context, cccid *ccprovider.CCContext, spec interface{}) (*pb.Response, *pb.ChaincodeEvent, error) { |
启动的动作在下面这个方法中完成:
1 | , cMsg, err := theChaincodeSupport.Launch(ctxt, cccid, spec) |
此方法的核心又是:launchAndWaitForRegister()
,负责具体的链码容器工作,代码位置:**/core/chaincode/chaincode_support.go/launchAndWaitForRegister**
1 | func (chaincodeSupport *ChaincodeSupport) launchAndWaitForRegister(ctxt context.Context, cccid *ccprovider.CCContext, cds *pb.ChaincodeDeploymentSpec, launcher launcherIntf) error { |
至此,chaincode.Execute()
函数检查并启动了链码容器,执行完成链码请求操作.
处理模拟执行结果
处理模拟执行结果是由下面几段代码实现的:
1 | //=== 获取并处理交易模拟执行结果 |
两个关键函数:一个是GetTxSimulationResults
,还有一个就是distributePrivateData
。
GetTxSimulationResults
主要获取交易模拟执行结果的隐私数据读写集,然后遍历计算集合隐私数据的哈希值,然后获取交易模拟执行结果的公有数据读写集,最后构造交易模拟执行结果TxSimulationResults
结构对象并返回。
distributePrivateData
首先会获取指定通道上的隐私数据处理句柄,然后通过handler.distributor.Distribute
分发隐私数据,
最后通过coordinator
模块将指定交易txID
的隐私数据读写集privData
暂时保存到本地transient
隐私数据库中。Committer
记账节点在提交区块数据与隐私数据之后,主动删除transient
隐私数据库中关联的隐私数据,以及时清理过期数据。
对模拟执行结果签名背书
endorseProposal()
方法对模拟执行结果进行签名背书,并返回提案响应消息。
1 | func (e *Endorser) endorseProposal(...) (*pb.ProposalResponse, error) { |
callChaincode()
方法调用ESCC
系统链码的EndorserOneValidSignature.Invoke
()方法,对模拟结果执行签名背书操作。代码如下:
位置:/core/scc/escc/endorser_onevalidsignature.go/Invoke
1 | func (e *EndorserOneValidSignature) Invoke(stub shim.ChaincodeStubInterface) pb.Response { |
至此,Endorser
背书节点处理签名提案消息的流程结束。