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)对模拟结果签名背书
func (e *Endorser) ProcessProposal(ctx context.Context, signedProp *pb.SignedProposal) (*pb.ProposalResponse, error) {
...
endorserLogger.Debug("Entering: request from", addr)
var chainID string
var hdrExt *pb.ChaincodeHeaderExtension
var success bool
defer 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 = true
return pResp, nil
}
0、数据流梳理
由于源码涉及很多的数据结构,现在挑选关键几个流程的数据流进行梳理,方便在看到对应结构体的时候看到所在的位置。
1)提案请求数据流
如下图。背书节点收到的请求结构体是pb.SignedProposal,SDK发起的请求功能函数与参数在标黄部分,实际该部分的功能函数与合约是一致的
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 []byte
var 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-ChaincodeVersion
func (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() // 获取容器ID
if 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侧发送与接收消息。链码容器启动流程如下:
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
// 模拟执行提案
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 = true
return pResp, nil
}
背书签名的处理函数endorseProposal,最后调用plugin.Endorse接口对返回的公有数据进行签名,并对数据进行序列化,之后返回给原函数。