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节点。
至此背书流程结束。
二、源码分析
了解了背书大致流程之后,这部分会介绍背书节点的详细工作(源码分析)
背书处理流程的入口函数是(e *Endorser) ProcessProposal(fabric\core\endorser\endorser.go)。该函数是在peer启动时注册在背书服务的处理函数。
主要分为4个功能模块:(1)提案消息预处理(2)模拟执行提案(3)处理模拟执行结果(4)对模拟结果签名背书
1 | func (e *Endorser) ProcessProposal(ctx context.Context, signedProp *pb.SignedProposal) (*pb.ProposalResponse, error) { |
0、数据流梳理
由于源码涉及很多的数据结构,现在挑选关键几个流程的数据流进行梳理,方便在看到对应结构体的时候看到所在的位置。
1)提案请求数据流
如下图。背书节点收到的请求结构体是pb.SignedProposal,SDK发起的请求功能函数与参数在标黄部分,实际该部分的功能函数与合约是一致的
1、预处理提案信息
preProcess()方法负责预处理签名提案消息,包括验证提案消息格式与签名的合法性、检查提案消息是否为允许通过外部调用的系统链码、检查交易的唯一性、检查签名提案消息是否满足指定的通道访问权限策略等。
1 | // 预处理提案信息 |
2、模拟执行交易
1 | // 模拟执行提案 |
callChaincode接口最后调用(cs *ChaincodeSupport) Invoke使用合约服务来模拟执行合约。我们可以看到这个功能函数很简单,只有2个:启动合约容器;执行合约
1 | func (cs *ChaincodeSupport) Invoke(txParams *ccprovider.TransactionParams, cccid *ccprovider.CCContext, input *pb.ChaincodeInput) (*pb.ChaincodeMessage, error) { |
1)启动用户链码Docker容器
Launch()接口最后会调用(vm *DockerVM) Start接口创建用户合约容器
1 | // 创建用户合约容器 |
用户合约在启动执行时会先调用Peer节点上链码支持服务器的Register()服务接口,请求与Peer侧链码支持服务器建立连接,获取gRPC服务客户端通信流和Peer侧发送与接收消息。链码容器启动流程如下:
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()方法,并且该链码已经完成了初始化工作。
3、处理模拟执行结果
启动链码容器与初始化链码执行环境,模拟执行合法的签名提案消息,并将模拟执行结果记录在交易模拟器中。其中,对公有数据(包含公共数据与隐私数据哈希值)继续签名背书,并提交给Orderer节点请求排序出块,同时将隐私数据通过Gossip消息协议发送到组织内的其他授权节点上。
callChaincode返回执行交易结果,对结果进行处理。通过合法的交易模拟器调用txsim.GetTxSimulationResults()方法,获取当前交易模拟器rwsetBuilder对象保存的模拟执行结果读写集simResult,包括公有数据读写集PubSimulationResults(含有公共数据与隐私数据哈希值)和隐私数据读写集PvtSimulationResults
1 | // 模拟执行提案 |
4、对模拟结果签名背书
结果返回到初始的处理函数
1 | func (e *Endorser) ProcessProposal(ctx context.Context, signedProp *pb.SignedProposal) (*pb.ProposalResponse, error) { |
背书签名的处理函数endorseProposal,最后调用plugin.Endorse接口对返回的公有数据进行签名,并对数据进行序列化,之后返回给原函数。