Endorser背书都干了什么?

一、背书流程

Peer节点中某些节点会充当背书节点的角色。在SDK发起背书请求之后,背书节点进行响应。

背书节点对请求服务的签名提案消息执行启动链码容器、模拟执行提案、签名背书的流程。

Endorser背书节点在链码模拟执行结束后调用ESCC背书管理系统链码,利用本地签名者实体对模拟执行结果进行签名背书,然后将模拟执行结果、背书签名、链码执行响应消息等封装为提案响应消息(ProposalResponse类型),并回复给请求客户端。 所有客户端提交到账本的普通交易消息都需要经过指定的Endorser背书节点签名认可,并在检查收到足够的签名背书信息之后,才能将签名提案消息、模拟执行结果及其背书信息等打包成签名交易消息(Envelope类型),发送给Orderer节点请求排序出块。

背书流程Peer节点需要2个服务:7051的背书服务和7052的链码服务。这2个服务在peer启动的时候均已经注册完成(详见peer的启动流程)

注:SDK与Peer节点交互是grpc协议,Peer与Chaincode交互是grpc协议。

简单的流程如下图:SDK发起业务请求,在Fabric-sdk封装提案请求,并对请求进行签名,作为grpc的客户端发送背书请求,背书服务监听到背书请求,开始进行处理。首先会对请求报文进行预处理,包括验证提案消息格式与签名的合法性、检查提案消息是否为允许通过外部调用的系统链码、检查交易的唯一性、检查签名提案消息是否满足指定的通道访问权限策略等。接下来会启动链码容器,若没有创建会先进行创建合约容器(所以这就是为什么第一次发起请求会时间较长的原因),创建好容器之后会模拟执行交易,使用链码服务与链码容器进行交互,链码容器是无状态的,所以数据访问状态数据库都是要经过peer进行访问,执行完成之后背书节点会处理数据,将数据分为公有数据和私有数据,公有数据签名打包返回给SDK客户端,私有数据Gossip协议分发给各个peer节点。

Fabric-peer背书源码分析(二)_Fabric-peer

至此背书流程结束。

二、源码分析

了解了背书大致流程之后,这部分会介绍背书节点的详细工作(源码分析)

背书处理流程的入口函数是(e *Endorser) ProcessProposal(fabric\core\endorser\endorser.go)。该函数是在peer启动时注册在背书服务的处理函数。

主要分为4个功能模块:(1)提案消息预处理(2)模拟执行提案(3)处理模拟执行结果(4)对模拟结果签名背书

func (e *Endorser) ProcessProposal(ctx context.Context, signedProp *pb.SignedProposal) (*pb.ProposalResponse, error) {
	...
	endorserLogger.Debug("Entering: request from", addr)	var chainID stringvar hdrExt *pb.ChaincodeHeaderExtension	var success booldefer func() {		if hdrExt != nil {// 处理错误信息
			...
		}
		endorserLogger.Debug("Exit: request from", addr)
	}()	// 0 --预处理提案信息,获取提案信息,链相关的信息
	vr, err := e.preProcess(signedProp)	if err != nil {
		resp := vr.resp		return resp, err
	}

	prop, hdrExt, chainID, txid := vr.prop, vr.hdrExt, vr.chainID, vr.txid	// 创建交易模拟器与历史查询执行器var txsim ledger.TxSimulator	var historyQueryExecutor ledger.HistoryQueryExecutor	if acquireTxSimulator(chainID, vr.hdrExt.ChaincodeId) {		// 创建交易模拟器对象// 创建该交易的交易模拟器if txsim, err = e.s.GetTxSimulator(chainID, txid); err != nil {			return &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}, nil
		}		defer txsim.Done()// 获取历史查询执行器if historyQueryExecutor, err = e.s.GetHistoryQueryExecutor(chainID); err != nil {			return &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}, nil
		}
	}

	txParams := &ccprovider.TransactionParams{
		ChannelID:            chainID,
		TxID:                 txid,
		SignedProp:           signedProp,
		Proposal:             prop,
		TXSimulator:          txsim,
		HistoryQueryExecutor: historyQueryExecutor,
	}	// 1 --模拟执行交易提案
	cd, res, simulationResult, ccevent, err := e.SimulateProposal(txParams, hdrExt.ChaincodeId)	if err != nil {		return &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}, nil
	}	// 处理交易模拟运行结果的响应消息if res != nil {           // 创建背书失败的提案响应消息
			...        	return pResp, nil
		}
	}	// 2 -- 处理交易模拟结果var pResp *pb.ProposalResponse	if chainID == "" {		// 链ID为空,则不需要背书,直接构造提案响应消息返回
		pResp = &pb.ProposalResponse{Response: res}
	} else {		// 3 -- 对模拟执行结果进行签名
		pResp, err = e.endorseProposal(ctx, chainID, txid, signedProp, prop, res, simulationResult, ccevent, hdrExt.PayloadVisibility, hdrExt.ChaincodeId, txsim, cd)		// if error, capture endorsement failure metric
		meterLabels := []string{			"channel", chainID,			"chaincode", hdrExt.ChaincodeId.Name + ":" + hdrExt.ChaincodeId.Version,
		}		if err != nil {			// 处理错误信息...
		}		if pResp.Response.Status >= shim.ERRORTHRESHOLD {			// 处理错误信息return pResp, nil
		}
	}	// 将处理结果返回给客户端
	pResp.Response = res

	e.Metrics.SuccessfulProposals.Add(1)
	success = truereturn pResp, nil}复制代码
0、数据流梳理

由于源码涉及很多的数据结构,现在挑选关键几个流程的数据流进行梳理,方便在看到对应结构体的时候看到所在的位置。

1)提案请求数据流

如下图。背书节点收到的请求结构体是pb.SignedProposal,SDK发起的请求功能函数与参数在标黄部分,实际该部分的功能函数与合约是一致的

Fabric-peer背书源码分析(二)_Fabric-peer_02

1、预处理提案信息

preProcess()方法负责预处理签名提案消息,包括验证提案消息格式与签名的合法性、检查提案消息是否为允许通过外部调用的系统链码、检查交易的唯一性、检查签名提案消息是否满足指定的通道访问权限策略等。

// 预处理提案信息func (e *Endorser) preProcess(signedProp *pb.SignedProposal) (*validateResult, error) {// 返回结果
	vr := &validateResult{}    
	// 1、验证签名提案消息格式与签名的合法性 同时获取提案消息、消息头部及其扩展域
	prop, hdr, hdrExt, err := validation.ValidateProposalMessage(signedProp)	if err != nil {
		e.Metrics.ProposalValidationFailed.Add(1)
		vr.resp = &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}		return vr, err
	}	// 2、获取消息通道头部
	chdr, err := putils.UnmarshalChannelHeader(hdr.ChannelHeader)	if err != nil {
		vr.resp = &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}		return vr, err
	}	// 3、获取消息签名头部
	shdr, err := putils.GetSignatureHeader(hdr.SignatureHeader)	if err != nil {
		vr.resp = &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}		return vr, err
	}	// 若是系统链码,检查是否为允许从外部调用的系统链码,cscc lscc qscc// 3、检查提案消息头部的hdrExt.ChaincodeId.Name链码名称是否为支持从外部调用的系统链码(CSCC、LSCC与QSCC)if e.s.IsSysCCAndNotInvokableExternal(hdrExt.ChaincodeId.Name) {
		...		return vr, err
	}	// 4、检查签名提案消息的唯一性
	chainID := chdr.ChannelId
	txid := chdr.TxId
	endorserLogger.Debugf("[%s][%s] processing txid: %s", chainID, shorttxid(txid), txid)	if chainID != "" {		// 校验txid,确保没有重复
		meterLabels := []string{			"channel", chainID,			"chaincode", hdrExt.ChaincodeId.Name + ":" + hdrExt.ChaincodeId.Version,
		}		// err == nil 查询成功,说明链上已有这个交易,重复执行则报错if _, err = e.s.GetTransactionByID(chainID, txid); err == nil {
			...			return vr, err
		}		// 检查签名提案消息是否是满足指定通道的访问权限策略,以确保允许交易// 确保是用户链码if !e.s.IsSysCC(hdrExt.ChaincodeId.Name) {			// 检查提案是否符合WRITER写通道权限策略if err = e.s.CheckACL(signedProp, chdr, shdr, hdrExt); err != nil {
				...				return vr, err
			}
		}
	} else { 
	}
	vr.prop, vr.hdrExt, vr.chainID, vr.txid = prop, hdrExt, chainID, txid	return vr, nil}复制代码
2、模拟执行交易
// 模拟执行提案func (e *Endorser) SimulateProposal(txParams *ccprovider.TransactionParams, cid *pb.ChaincodeID) (ccprovider.ChaincodeDefinition, *pb.Response, []byte, *pb.ChaincodeEvent, error) {
	...	// 获取ChaincodeInvocationSpec,封装了调用合约的方法和请求参数
	cis, err := putils.GetChaincodeInvocationSpec(txParams.Proposal)	if err != nil {		return nil, nil, nil, nil, err
	}	var cdLedger ccprovider.ChaincodeDefinition	var version string// 检查是否为系统链码if !e.s.IsSysCC(cid.Name) {		// 通过LSCC系统链码获取账本中保存的链码数据对象
		cdLedger, err = e.s.GetChaincodeDefinition(cid.Name, txParams.TXSimulator)		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 {		// 获取系统的版本
		version = util.GetSysCCVersion()
	}	var simResult *ledger.TxSimulationResults	var pubSimResBytes []bytevar res *pb.Response	var ccevent *pb.ChaincodeEvent	// 启动链码容器调用链码模拟执行
	res, ccevent, err = e.callChaincode(txParams, version, cis.ChaincodeSpec.Input, cid)	if err != nil {
		endorserLogger.Errorf("[%s][%s] failed to invoke chaincode %s, error: %+v", txParams.ChannelID, shorttxid(txParams.TxID), cid, err)		return nil, nil, nil, nil, err
	}	// 获取并处理交易模拟执行结果if txParams.TXSimulator != nil {		if simResult, err = txParams.TXSimulator.GetTxSimulationResults(); err != nil {
			txParams.TXSimulator.Done()			return nil, nil, nil, nil, err
		}		if simResult.PvtSimulationResults != nil {			// 获取因素数据
			pvtDataWithConfig, err := e.AssemblePvtRWSet(simResult.PvtSimulationResults, txParams.TXSimulator)
			txParams.TXSimulator.Done()
			...
			pvtDataWithConfig.EndorsedAt = endorsedAt			// 分发通道中隐私数据读写集if err := e.distributePrivateData(txParams.ChannelID, txParams.TxID, pvtDataWithConfig, endorsedAt); err != nil {				return nil, nil, nil, nil, err
			}
		}
		txParams.TXSimulator.Done()		// 获取模拟结果的公有数据if pubSimResBytes, err = simResult.GetPubSimulationBytes(); err != nil {			return nil, nil, nil, nil, err
		}
	}	return cdLedger, res, pubSimResBytes, ccevent, nil}复制代码

callChaincode接口最后调用(cs *ChaincodeSupport) Invoke使用合约服务来模拟执行合约。我们可以看到这个功能函数很简单,只有2个:启动合约容器;执行合约

func (cs *ChaincodeSupport) Invoke(txParams *ccprovider.TransactionParams, cccid *ccprovider.CCContext, input *pb.ChaincodeInput) (*pb.ChaincodeMessage, error) {// 1-- 启动合约容器
	h, err := cs.Launch(txParams.ChannelID, cccid.Name, cccid.Version, txParams.TXSimulator)	if err != nil {		return nil, err
	}
	cctype := pb.ChaincodeMessage_TRANSACTION	// 2-- 执行合约return cs.execute(cctype, txParams, cccid, input, h)
}复制代码
1)启动用户链码Docker容器

Launch()接口最后会调用(vm *DockerVM) Start接口创建用户合约容器

// 创建用户合约容器// 用户链码的容器名称是NetworkID-PeerID-ChaincodeName-ChaincodeVersionfunc (vm *DockerVM) Start(ccid ccintf.CCID, args, env []string, filesToUpload map[string][]byte, builder container.Builder) error {
	imageName, err := vm.GetVMNameForDocker(ccid)	if err != nil {		return err
	}

	attachStdout := viper.GetBool("vm.docker.attachStdout")
	containerName := vm.GetVMName(ccid)  // 获取镜像名称 
	logger := dockerLogger.With("imageName", imageName, "containerName", containerName)

	client, err := vm.getClientFnc() // 获取容器IDif err != nil {
		logger.Debugf("failed to get docker client", "error", err)		return err
	}

	vm.stopInternal(client, containerName, 0, false, false)	// 创建指定容器
	err = vm.createContainer(client, imageName, containerName, args, env, attachStdout)	// 若没有镜像if err == docker.ErrNoSuchImage {
		reader, err := builder.Build()		if err != nil {			return errors.Wrapf(err, "failed to generate Dockerfile to build %s", containerName)
		}		// 构建并部署docker 镜像
		err = vm.deployImage(client, ccid, reader)		if err != nil {			return err
		}		// 创建容器
		err = vm.createContainer(client, imageName, containerName, args, env, attachStdout)		if err != nil {
			logger.Errorf("failed to create container: %s", err)			return err
		}
	} else if err != nil {
		logger.Errorf("create container failed: %s", err)		return err
	}
	....	// 开启容器
	err = client.StartContainer(containerName, nil)	if err != nil {
		dockerLogger.Errorf("start-could not start container: %s", err)		return err
	}

	dockerLogger.Debugf("Started container %s", containerName)	return nil}复制代码

用户合约在启动执行时会先调用Peer节点上链码支持服务器的Register()服务接口,请求与Peer侧链码支持服务器建立连接,获取gRPC服务客户端通信流和Peer侧发送与接收消息。链码容器启动流程如下:

Fabric-peer背书源码分析(二)_Fabric-peer_03

1)Peer侧调用ChaincodeSupport.Launch()方法启动链码容器。Peer侧调用HandleChaincodeStream()函数与链码容器侧调用chatWithPeer()函数,分别创建两侧对应的Handler对象及其服务状态,建立消息处理循环与通信机制(Golang通道或gRPC通信流)。同时,将两侧服务状态设置为created。链码容器侧构造链码ChaincodeID结构对象chaincodeID,封装了链码名称,并将该对象序列化后封装到ChaincodeMessage_REGISTER类型的链码消息中作为消息负载。然后,调用handler. serialSend()方法,将该消息发送到Peer侧请求注册,将Handler对象(含有与当前链码容器侧连接的通信流)注册到Peer侧的链码服务实例对象上提供服务。

2)Peer侧收到来自链码容器侧的ChaincodeMessage_REGISTER类型消息,交由handler.handleMessage()→Handler.beforeRegisterEvent()方法处理。回复ChaincodeMessage_REGISTERED消息到链码容器侧以通知注册完成。同时将当前状态由created转换为established

3)链码容器侧接收到ChaincodeMessage_REGISTERED消息之后,交由handler.handleMessage()→handler.beforeRegistered()方法进行处理,在检查消息的合法性之后,更新自身状态由created转换为established。

4)然后,Peer侧调用chaincodeSupport.sendReady()→chrte.handler.ready()方法,构造ChaincodeMessage_READY类型的链码消息,首先触发Peer侧的状态由established转换为ready,再将ChaincodeMessage_READY类型的链码消息发送到链码容器侧,通知将状态转换为ready状态,做好链码初始化与调用前的准备工作。至此,链码容器就正式启动完毕。Peer侧与链码容器侧都启动了Handler消息处理句柄与消息处理循环,两侧都处于ready状态,等待执行链码

2)执行链码

(h *Handler) Execute执行链码。执行流程如下图:

Peer侧发送ChaincodeMessage_TRANSACTION类型的链码消息到链码容器侧,请求调用链码的Invoke()方法,并且该链码已经完成了初始化工作。

Fabric-peer背书源码分析(二)_Fabric-peer_04

3、处理模拟执行结果

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

callChaincode返回执行交易结果,对结果进行处理。通过合法的交易模拟器调用txsim.GetTxSimulationResults()方法,获取当前交易模拟器rwsetBuilder对象保存的模拟执行结果读写集simResult,包括公有数据读写集PubSimulationResults(含有公共数据与隐私数据哈希值)和隐私数据读写集PvtSimulationResults

// 模拟执行提案func (e *Endorser) SimulateProposal(txParams *ccprovider.TransactionParams, cid *pb.ChaincodeID) (ccprovider.ChaincodeDefinition, *pb.Response, []byte, *pb.ChaincodeEvent, error) {
	...	// 启动链码容器调用链码模拟执行
	res, ccevent, err = e.callChaincode(txParams, version, cis.ChaincodeSpec.Input, cid)	if err != nil {
		...
	}	// 获取并处理交易模拟执行结果if txParams.TXSimulator != nil {		if simResult, err = txParams.TXSimulator.GetTxSimulationResults(); err != nil {
			txParams.TXSimulator.Done()			return nil, nil, nil, nil, err
		}		if simResult.PvtSimulationResults != nil {
			...// 获取私有数据的读写集
			pvtDataWithConfig, err := e.AssemblePvtRWSet(simResult.PvtSimulationResults, txParams.TXSimulator)
			...
			txParams.TXSimulator.Done()
			...			// 分发通道中隐私数据读写集,具体分发是gossip协议,本文先不做讨论if err := e.distributePrivateData(txParams.ChannelID, txParams.TxID, pvtDataWithConfig, endorsedAt); err != nil {				return nil, nil, nil, nil, err
			}
		}

		txParams.TXSimulator.Done()		// 获取模拟结果的公有数据,后续对共有数据进行签名if pubSimResBytes, err = simResult.GetPubSimulationBytes(); err != nil {			return nil, nil, nil, nil, err
		}
	}	return cdLedger, res, pubSimResBytes, ccevent, nil}复制代码
4、对模拟结果签名背书

结果返回到初始的处理函数

func (e *Endorser) ProcessProposal(ctx context.Context, signedProp *pb.SignedProposal) (*pb.ProposalResponse, error) {
	...	// 1 --模拟执行交易提案
	cd, res, simulationResult, ccevent, err := e.SimulateProposal(txParams, hdrExt.ChaincodeId)
	...	if chainID == "" {		// 链ID为空,则不需要背书,直接构造提案响应消息返回
		pResp = &pb.ProposalResponse{Response: res}
	} else {		// 3 -- 对模拟执行结果进行签名背书
		pResp, err = e.endorseProposal(ctx, chainID, txid, signedProp, prop, res, simulationResult, ccevent, hdrExt.PayloadVisibility, hdrExt.ChaincodeId, txsim, cd)
		...
	}	// 将处理结果返回给客户端
	pResp.Response = res

	e.Metrics.SuccessfulProposals.Add(1)
	success = truereturn pResp, nil}复制代码

背书签名的处理函数endorseProposal,最后调用plugin.Endorse接口对返回的公有数据进行签名,并对数据进行序列化,之后返回给原函数。