阅读 122

死磕hyperledger fabric源码|Endorser节点背书服务

死磕hyperledger fabric源码|Endorser节点背书服务

文章及代码:github.com/blockchainG…

分支:v1.1.0

77613ebad5a9eb5ea42dbb3b323fdaf5

背书概述

Endorser背书节点提供ProcessProposal()服务接口用于接收与处理签名提案消息的请求,启动用户链码容器,执行调用链码,并对模拟执行结果进行签名背书,。Peer节点启动时解析core.yaml文件中的peer.handlers配置项,并构造认证过滤器列表。如果存在合法类型的认证过滤器,则需要先经过所有认证过滤器调用ProcessProposal()方法进行验证过滤,例如检查身份证书是否过期,然后再提交给背书服务器的serverEndorser.ProcessProposal()方法进行处理。 方法功能如下:

func (e *Endorser) ProcessProposal(ctx context.Context, signedProp *pb.SignedProposal) (*pb.ProposalResponse, error) {
	...
	//检查并检验签名提案消息的合法性
	vr, err := e.preProcess(signedProp)
	...
	// 创建交易模拟器与历史查询执行器
	var txsim ledger.TxSimulator
	var historyQueryExecutor ledger.HistoryQueryExecutor
	if chainID != "" {
		// 创建交易模拟器对象
		if txsim, err = e.s.GetTxSimulator(chainID, txid); err != nil {
			return &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}, err
		}
		if historyQueryExecutor, err = e.s.GetHistoryQueryExecutor(chainID); err != nil {
			// 创建历史查询器对象
			return &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}, err
		}
		// 将历史查询执行器添加到context中的KV键值对
		ctx = context.WithValue(ctx, chaincode.HistoryQueryExecutorKey, historyQueryExecutor)
	}
	
	// 模拟交易执行
	cd, res, simulationResult, ccevent, err := e.simulateProposal(ctx, chainID, txid, signedProp, prop, hdrExt.ChaincodeId, txsim)
	if err != nil {
		// 检查交易模拟运行结果的响应消息
		return &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}, err
	}
	if res != nil {
		...
			// 创建背书失败的提案响应消息
			pResp, err := putils.CreateProposalResponseFailure(prop.Header, prop.Payload, res, simulationResult, cceventBytes, hdrExt.ChaincodeId, hdrExt.PayloadVisibility)
			if err != nil {
				return &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}, err
			}

			return pResp, &chaincodeError{res.Status, res.Message}
		}
	}

	// 调用ESCC系统链码对模拟执行结果进行背书,并回复提案响应消息
	var pResp *pb.ProposalResponse
	if chainID == "" {
		pResp = &pb.ProposalResponse{Response: res}
	} else { // 签名背书
		pResp, err = e.endorseProposal(ctx, chainID, txid, signedProp, prop, res, simulationResult, ccevent, hdrExt.PayloadVisibility, hdrExt.ChaincodeId, txsim, cd)
		if err != nil {
			return &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}, err
		}
		if pResp != nil {
			if res.Status >= shim.ERRORTHRESHOLD { // 检查响应消息是否存在错误
				endorserLogger.Debugf("[%s][%s] endorseProposal() resulted in chaincode %s error for txid: %s", chainID, shorttxid(txid), hdrExt.ChaincodeId, txid)
				return pResp, &chaincodeError{res.Status, res.Message}
			}
		}
	}
	pResp.Response.Payload = res.Payload // 设置链码提案响应消息负载字节数组,含有链码调用返回值
复制代码

主要做了以下几件事:

  1. 调用preProcess()方法预处理签名提案消息,验证消息合法性
  2. 调用simulateProposal()方法启动链码容器并模拟执行提案,将结果读写集记录到模拟交易器中。
  3. 调用endorseProposal()方法对模拟执行结果进行签名背书,并返回提案响应消息。

下面的内容将会紧紧围绕这几部分来进行分析。

预处理签名提案消息

进入到preProcess函数:

①: 验证签名提案消息格式与签名的合法性

prop, hdr, hdrExt, err := validation.ValidateProposalMessage(signedProp)
复制代码

②: 检查提案消息是否允许外部调用的系统链码

//解析消息通道头部ChannelHeader结构
	chdr, err := putils.UnmarshalChannelHeader(hdr.ChannelHeader)
	...
	//解析消息签名头部SignatureHeader结构
	shdr, err := putils.GetSignatureHeader(hdr.SignatureHeader)
	...
	//如果是系统链码,则检查是否为允许从外部调用的系统链码:cscc、lscc或qscc
	if e.s.IsSysCCAndNotInvokableExternal(hdrExt.ChaincodeId.Name) {
		endorserLogger.Errorf("Error: an attempt was made by %#v to invoke system chaincode %s",
			shdr.Creator, hdrExt.ChaincodeId.Name)
		err = errors.Errorf("chaincode %s cannot be invoked through a proposal", hdrExt.ChaincodeId.Name)
		//构造提案响应消息对象:状态码为500(错误)与错误信息
		vr.resp = &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}
		return vr, err
	}
复制代码

③:检查签名提案消息的唯一性以及是否满足指定通道的访问权限策略

chainID := chdr.ChannelId //获取通道标识号ChannelID,即链chainID
	//// 检查账本中交易ID的唯一性。注意ValidateProposalMessage()方法已经验证了交易号ID的合法性
	txid := chdr.TxId
	if txid == "" {
		err = errors.New("invalid txID. It must be different from the empty string")
		vr.resp = &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}
		return vr, err
	}
	endorserLogger.Debugf("[%s][%s] processing txid: %s", chainID, shorttxid(txid), txid)
	if chainID != "" {
		// 根据交易ID从账本中获取指定的交易对象,检查账本中交易对象的唯一性,
		// 若找到该对象则说明重复发起了交易,此时应报错
		if _, err = e.s.GetTransactionByID(chainID, txid); err == nil {
			return vr, errors.Errorf("duplicate transaction found [%s]. Creator [%x]", txid, shdr.Creator)
		}
		/// 检查是否为系统链码,确保是用户链码
		if !e.s.IsSysCC(hdrExt.ChaincodeId.Name) {
			//// 检查提案是否符合WRITER写通道权限策略
			if err = e.s.CheckACL(signedProp, chdr, shdr, hdrExt); err != nil {
				vr.resp = &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}
				return vr, err
			}
		}
  } else {}
复制代码

以上3个部分内容还需要进一步的细化,看接下来的分析。

验证消息格式与签名合法性

①:调用validation.ValidateProposalMessage()函数,以检查签名提案消息格式与签名的合法性,解析获取提案消息、消息头部及其扩展域。

1.1 校验header

chdr, shdr, err := validateCommonHeader(hdr)
复制代码

校验header里面大概做了这几件事:

  • validateChannelHeader(chdr)函数检查通道头部chdr的合法性,其通道头部类型应该属于ENDORSER_TRANSACTIONCONFIG_UPDATECONFIGPEER_RESOURCE_UPDATE,并且Epoch字段应该为0;

  • validateSignatureHeader(shdr)函数检查签名头部shdr的合法性,随机数Nonce和消息

    签名者Creator不应该为nil,并且该对象字节数不为0

1.2 检查消息签名的合法性

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是否计算正确

err = utils.CheckProposalTxID(
		chdr.TxId,
		shdr.Nonce,
		shdr.Creator)
复制代码

重新计算消息随机数Nonce(防止重放攻击)与签名者Creator组合信息后的哈希值,并且与交易ID进行比较。如果两者匹配相同,则说明交易ID是正确的。

检查是否为允许外部调用的系统链码

if e.s.IsSysCCAndNotInvokableExternal(hdrExt.ChaincodeId.Name) {
		endorserLogger.Errorf("Error: an attempt was made by %#v to invoke system chaincode %s",
			shdr.Creator, hdrExt.ChaincodeId.Name)
		err = errors.Errorf("chaincode %s cannot be invoked through a proposal", hdrExt.ChaincodeId.Name)
		//构造提案响应消息对象:状态码为500(错误)与错误信息
		vr.resp = &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}
		return vr, err
	}
复制代码

检查签名提案消息的唯一性

preProcess()方法继续检查签名提案消息的唯一性,以防止重放攻击。该方法从提案消息通道头部提取链与交易ID,包括两种情况:

  • 如果链ID不是空字符串,则需要检查该交易ID的唯一性,确保之前没有提交过该交易到账本中。即根据交易ID从账本的区块文件以及区块索引数据库获取交易数据与交易验证码,并构造成已处理的交易对象 。如果获取交易数据成功且没有错误,则说明账本中已经保存了指定交易ID的交易数据。因此,当前提案消息属于重复提交,报错返回。否则,就说明该签名提案消息通过了消息唯一性的检查;
  • 如果链ID是空字符串,则不需要检查签名提案消息的唯一性与验证通道访问权限策略,只需要通过ValidateProposalMessage()函数验证该提案消息的合法性即可。

检查是否满足通道的访问权限策略

首先调用IsSysCC函数,检查链码是否为系统链码。如果是用户链码,则调用 CheckACL方法,检查签名提案消息是否满足通道PROPOSE权限策略要求,以允许提交该消息到指定通道上继续进行处理。CheckACL方法如下:

func (d *defaultACLProvider) CheckACL(resName string, channelID string, idinfo interface{}) error {
  policy := d.defaultPolicy(resName, true)
  ....
  case *pb.SignedProposal:
		return d.policyChecker.CheckPolicy(channelID, policy, idinfo.(*pb.SignedProposal))
	case *common.Envelope:
		sd, err := idinfo.(*common.Envelope).AsSignedData()
		if err != nil {
			return err
		}
		return d.policyChecker.CheckPolicyBySignedData(channelID, policy, sd)
}
复制代码

方法先调用defaultPolicy()方法,从全局通道资源策略字典cResourcePolicyMap中获取指定策略名称resources.PROPOSE的默认策略。对于SignedProposal类型的签名提案消息,CheckACL()方法调用d.policyChecker.CheckPolicy()方法,检查该签名提案消息是否满足该通道上的Writers写权限策略要求。

模拟执行提案

ProcessProposal()方法启动链码容器初始化链码执行环境,模拟执行合法的签名提案消息,并将模拟执行结果记录在交易模拟器中。其中,对公有数据(包含公共数据与隐私数据哈希值)继续签名背书,并提交给Orderer节点请求排序出块,同时将隐私数据通过Gossip消息协议发送到组织内的其他授权节点上。核心函数如下:

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) {
	// 解析获取链码调用规范对象
	cis, err := putils.GetChaincodeInvocationSpec(prop)
	...
	//1 检查是否为系统链码
	if !e.s.IsSysCC(cid.Name) { // 如果是调用用户链码,则需要保证该链码已经实例化了
		// === 用户链码,通过调用LSCC系统链码获取账本中保存的链码数据对象ChaincodeData结构
		// 如果链上有链码数据对象,则说明链码已经成功实例化
		cdLedger, err = e.s.GetChaincodeDefinition(ctx, chainID, txid, signedProp, prop, cid.Name, txsim)
		if err != nil {
			return nil, nil, nil, nil, errors.WithMessage(err, fmt.Sprintf("make sure the chaincode %s has been successfully instantiated and try again", cid.Name))
		}
		// 获取已保存的链码版本
		version = cdLedger.CCVersion()
		// 检查提案中的实例化策略与调用账本中的实例化策略是否匹配
		err = e.s.CheckInstantiationPolicy(cid.Name, version, cdLedger)
		if err != nil {
			return nil, nil, nil, nil, err
		}
	} else { // === 执行系统链码,如lscc等
		version = util.GetSysCCVersion() // 获取系统链码版本
	}
	...
	// 2 启动链码容器调用链码
	res, ccevent, err = e.callChaincode(ctx, chainID, version, txid, signedProp, prop, cis, cid, txsim)
	if err != nil {
		endorserLogger.Errorf("[%s][%s] failed to invoke chaincode %s, error: %+v", chainID, shorttxid(txid), cid, err)
		return nil, nil, nil, nil, err
	}
	//3 获取并处理交易模拟执行结果
	if txsim != nil {
		if simResult, err = txsim.GetTxSimulationResults(); err != nil {
			return nil, nil, nil, nil, err
		}

		if simResult.PvtSimulationResults != nil { // 检查模拟结果隐私数据的合法性
			if cid.Name == "lscc" {
				// TODO: remove once we can store collection configuration outside of LSCC
				// 分发隐私数据
				return nil, nil, nil, nil, errors.New("Private data is forbidden to be used in instantiate")
			}
			if err := e.distributePrivateData(chainID, txid, simResult.PvtSimulationResults); err != nil {
				return nil, nil, nil, nil, err
			}
		}
		// 分发隐私数据
		if pubSimResBytes, err = simResult.GetPubSimulationBytes(); err != nil {
			return nil, nil, nil, nil, err
		}
	}
	return cdLedger, res, pubSimResBytes, ccevent, nil
}
复制代码

根据链码类型执行不同实例化策略

首先调用GetChaincodeInvocationSpec函数,从提案消息中解析提取出链码调用规范对象,然后调用IsSysCC(cid.Name)方法,依次匹配默认的系统链码名称,以判断当前链码类型是用户链码还是系统链码,分为用户链码和系统链码两种情况检查实例化策略。

①:用户链码

if !e.s.IsSysCC(cid.Name) { // 如果是调用用户链码,则需要保证该链码已经实例化了
		// === 用户链码,通过调用LSCC系统链码获取账本中保存的链码数据对象ChaincodeData结构
		// 如果链上有链码数据对象,则说明链码已经成功实例化
		cdLedger, err = e.s.GetChaincodeDefinition(ctx, chainID, txid, signedProp, prop, cid.Name, txsim)
		if err != nil {
			return nil, nil, nil, nil, errors.WithMessage(err, fmt.Sprintf("make sure the chaincode %s has been successfully instantiated and try again", cid.Name))
		}
		// 获取已保存的链码版本
		version = cdLedger.CCVersion()
		// 检查提案中的实例化策略与调用账本中的实例化策略是否匹配
		err = e.s.CheckInstantiationPolicy(cid.Name, version, cdLedger)
		if err != nil {
			return nil, nil, nil, nil, err
		}
复制代码

②:系统链码

else { // === 执行系统链码,如lscc等
		version = util.GetSysCCVersion() // 获取系统链码版本
	}
复制代码

启动链码容器

res, ccevent, err = e.callChaincode(ctx, chainID, version, txid, signedProp, prop, cis, cid, txsim)
复制代码

①:设置context上下文对象中交易模拟器的KV键值对,其中,键为TXSimulatorKey,值为交易模拟器txsim

if txsim != nil {
		ctxt = context.WithValue(ctxt, chaincode.TXSimulatorKey, txsim)
	}
复制代码

②:根据链码名称检查是否为系统链码

scc := e.s.IsSysCC(cid.Name)
复制代码

③:执行链码调用

res, ccevent, err = e.s.Execute(ctxt, chainID, cid.Name, version, txid, scc, signedProp, prop, cis)
复制代码

④:检查调用链码名称lscc

// 第1个参数为deploy部署或upgrade升级,第2个参数是链ID,第3个是链码部署规范对象
	if cid.Name == "lscc" && len(cis.ChaincodeSpec.Input.Args) >= 3 && (string(cis.ChaincodeSpec.Input.Args[0]) == "deploy" || string(cis.ChaincodeSpec.Input.Args[0]) == "upgrade") {
		var cds *pb.ChaincodeDeploymentSpec
		// 获取并验证链码部署规范
		cds, err = putils.GetChaincodeDeploymentSpec(cis.ChaincodeSpec.Input.Args[2])
		if err != nil {
			return nil, nil, err
		}

		//this should not be a system chaincode
		// 若试图部署/升级系统链码,则报错
		if e.s.IsSysCC(cds.ChaincodeSpec.ChaincodeId.Name) {
			return nil, nil, errors.Errorf("attempting to deploy a system chaincode %s/%s", cds.ChaincodeSpec.ChaincodeId.Name, chainID)
		}
		// 执行部署/升级链码
		_, _, err = e.s.Execute(ctxt, chainID, cds.ChaincodeSpec.ChaincodeId.Name, cds.ChaincodeSpec.ChaincodeId.Version, txid, false, signedProp, prop, cds)
		if err != nil {
			return nil, nil, err
		}
复制代码

启动的真正过程正是在e.s.Execute中完成的,分析如下:

/core/chaincode/exectransaction.go/Execute()

func Execute(ctxt context.Context, cccid *ccprovider.CCContext, spec interface{}) (*pb.Response, *pb.ChaincodeEvent, error) {
	...
	// === 设置初始链码消息对象
	// 部署(实例化)deploy命令或升级upgrade命令:调用链码Init()接口方法
	cctyp := pb.ChaincodeMessage_INIT
	//// 检查链码规范对象类型为ChaincodeDeploymentSpec或ChaincodeInvocationSpec
	if cds, _ = spec.(*pb.ChaincodeDeploymentSpec); cds == nil {
		if ci, _ = spec.(*pb.ChaincodeInvocationSpec); ci == nil {
			panic("Execute should be called with deployment or invocation spec")
		}
		// 调用invoke或查询query命令等:调用链码Invoke()接口方法
		cctyp = pb.ChaincodeMessage_TRANSACTION
	}

	// === 启动链码容器,返回链码输入参数等
	// created->established->ready状态
	_, cMsg, err := theChaincodeSupport.Launch(ctxt, cccid, spec)
	...
	// === 模拟执行交易链码并等待完成,监听并返回resp响应结果消息
	resp, err := theChaincodeSupport.Execute(ctxt, cccid, ccMsg, theChaincodeSupport.executetimeout)
	...
	// === 处理模拟执行结果
	if resp.ChaincodeEvent != nil {
		....
	}
		....
}
复制代码

启动的动作在下面这个方法中完成:

, cMsg, err := theChaincodeSupport.Launch(ctxt, cccid, spec)
复制代码

此方法的核心又是:launchAndWaitForRegister(),负责具体的链码容器工作,代码位置:/core/chaincode/chaincode_support.go/launchAndWaitForRegister

func (chaincodeSupport *ChaincodeSupport) launchAndWaitForRegister(ctxt context.Context, cccid *ccprovider.CCContext, cds *pb.ChaincodeDeploymentSpec, launcher launcherIntf) error {
	...
	// 如果chaincodeMap字典中已经存在对应的链码规范名称,则说明已经启动链码容器,此时直接返回即可
	if _, hasBeenLaunched := chaincodeSupport.chaincodeHasBeenLaunched(canName); hasBeenLaunched {
		...
	}
	// 检查该链码容器是否已经正常运行,直接返回
	if chaincodeSupport.launchStarted(canName) {
		...
	}
	...
		// 核心方法:启动容器,实际调用的是ccLauncherImpl方法
		resp, err := launcher.launch(ctxt, notfy)
	...
	// === 阻塞等待处理响应消息,等待REGISTER链码消息
	select {
	case ok := <-notfy:
		// Peer侧接收到链码容器侧发来的REGISTER注册链码消息,触发Handler的FSM运行,
		// 在回调方法beforeregister()中将外层Handler传递的notfy通道注册到Peer侧Handler中,
		// 根据链码注册成功结果,将结果消息放入notfy通道,触发此处的select语句。
		// 若notfy为flase,则说明注册失败。反之,则说明注册成功
		...
}
复制代码

至此,chaincode.Execute()函数检查并启动了链码容器,执行完成链码请求操作.

处理模拟执行结果

处理模拟执行结果是由下面几段代码实现的:

//=== 获取并处理交易模拟执行结果
	if txsim != nil {
		if simResult, err = txsim.GetTxSimulationResults(); err != nil {
			return nil, nil, nil, nil, err
		}

		if simResult.PvtSimulationResults != nil { // 检查模拟结果隐私数据的合法性
			if cid.Name == "lscc" {
				// TODO: remove once we can store collection configuration outside of LSCC
				return nil, nil, nil, nil, errors.New("Private data is forbidden to be used in instantiate")
			}
			// 分发隐私数据
			if err := e.distributePrivateData(chainID, txid, simResult.PvtSimulationResults); err != nil {
				return nil, nil, nil, nil, err
			}
		}

		if pubSimResBytes, err = simResult.GetPubSimulationBytes(); err != nil {
			return nil, nil, nil, nil, err
		}
	}
复制代码

两个关键函数:一个是GetTxSimulationResults,还有一个就是distributePrivateData

GetTxSimulationResults主要获取交易模拟执行结果的隐私数据读写集,然后遍历计算集合隐私数据的哈希值,然后获取交易模拟执行结果的公有数据读写集,最后构造交易模拟执行结果TxSimulationResults结构对象并返回。

distributePrivateData首先会获取指定通道上的隐私数据处理句柄,然后通过handler.distributor.Distribute分发隐私数据,

最后通过coordinator模块将指定交易txID的隐私数据读写集privData暂时保存到本地transient隐私数据库中。Committer记账节点在提交区块数据与隐私数据之后,主动删除transient隐私数据库中关联的隐私数据,以及时清理过期数据。

对模拟执行结果签名背书

endorseProposal()方法对模拟执行结果进行签名背书,并返回提案响应消息。

func (e *Endorser) endorseProposal(...) (*pb.ProposalResponse, error) {
	...
	// 调用ESCC系统链码进行背书
	res, _, err := e.callChaincode(ctx, chainID, version, txid, signedProp, proposal, ecccis, &pb.ChaincodeID{Name: escc}, txsim)
	...
}
复制代码

callChaincode()方法调用ESCC系统链码的EndorserOneValidSignature.Invoke()方法,对模拟结果执行签名背书操作。代码如下:

位置:/core/scc/escc/endorser_onevalidsignature.go/Invoke

func (e *EndorserOneValidSignature) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
	args := stub.GetArgs() // 获取参数列表
	// 检测参数个数
	if len(args) < 6 {
		return shim.Error(fmt.Sprintf("Incorrect number of arguments (expected a minimum of 5, provided %d)", len(args)))
	} else if len(args) > 8 {
		return shim.Error(fmt.Sprintf("Incorrect number of arguments (expected a maximum of 7, provided %d)", len(args)))
	}
	...
	// 获取执行链码响应消息
	response, err := putils.GetResponse(args[4])
	...
	// 获取模拟执行结果
	results = args[5]

	/..
	// 获取本地MSP组件
	localMsp := mspmgmt.GetLocalMSP()
	...
	// 获取本地默认签名者身份实体(即背书成员)
	signingEndorser, err := localMsp.GetDefaultSigningIdentity()
	...
	// 创建签名的提案响应消息
	presp, err := utils.CreateProposalResponse(hdr, payl, response, results, events, ccid, visibility, signingEndorser)
	...
	// 序列化提案响应字节数组
	prBytes, err := utils.GetBytesProposalResponse(presp)
	...
	// 回复执行成功消息
	return shim.Success(prBytes)
}

复制代码

至此,Endorser背书节点处理签名提案消息的流程结束。

参考

github.com/blockchainG…

文章分类
后端
文章标签