fabric源码解析11——peer的Admin和Endorser服务

fabric源码解析9——peer的Admin和Endorser服务

继续start.go的serve函数,peerServer对象在ChaincodeSupport服务之后,又注册了Admin,Endorser服务:

pb.RegisterAdminServer(peerServer.Server(), core.NewAdminServer())
serverEndorser := endorser.NewEndorserServer()
pb.RegisterEndorserServer(peerServer.Server(), serverEndorser)

其注册方式与注册ChaincodeSupport服务一致,在fabirc源码解析7中已经详述,所以本文只将目光锁定在Admin和Endorser服务本身。

Admin

Admin服务实现对服务器模块日志级别的获取和控制。服务原型定义在/fabric/protos/peer/admin.proto以及对应生成的admin.pb.go文件。核心代码在/fabric/core/admin.go中,该文件中实现了admin.pb.go中的Admin服务端服务。

//proto定义的服务原型和定义的服务状态
//在/fabric/protos/peer/admin.proto中定义
service Admin {
    rpc GetStatus(google.protobuf.Empty) returns (ServerStatus) {}
    rpc StartServer(google.protobuf.Empty) returns (ServerStatus) {}
    rpc StopServer(google.protobuf.Empty) returns (ServerStatus) {}
    ...
}
//proto使用枚举定义的服务状态
message ServerStatus {
    enum StatusCode {
        UNDEFINED = 0;
        STARTED = 1;
        STOPPED = 2;
        PAUSED = 3;
        ERROR = 4;
        UNKNOWN = 5;
    }//状态
    StatusCode status = 1;
}
//proto生成的服务端接口和注册函数
//在/fabric/protos/peer/admin.pb.go中定义
type AdminServer interface {
    GetStatus(context.Context, *google_protobuf.Empty) (*ServerStatus, error)
    StartServer(context.Context, *google_protobuf.Empty) (*ServerStatus, error)
    ...
}
func RegisterAdminServer(s *grpc.Server, srv AdminServer) {
    s.RegisterService(&_Admin_serviceDesc, srv)
}

//核心代码实现Admin服务
//在/fabric/core/admin.go中定义
type ServerAdmin struct {
}
//服务器的开启、停止、获取状态函数
func (*ServerAdmin) StartServer(context.Context, *empty.Empty) (*pb.ServerStatus, error) {
    status := &pb.ServerStatus{Status: pb.ServerStatus_STARTED}
    log.Debugf("returning status: %s", status
    return status, nil
}
func (*ServerAdmin) StartServer(...)...{...}
func (*ServerAdmin) GetStatus(...)...{...}
func (*ServerAdmin) StartServer(...)...{...}
//模块日志的获取、设置、恢复函数
func (*ServerAdmin) GetModuleLogLevel(...)...{...}
func (*ServerAdmin) SetModuleLogLevel(...)...{...}
func (*ServerAdmin) RevertLogLevels(...)...{...}

在此对Admin的StartServer和StopServer操作有一些疑问,字面上看来其是启动和停止服务,但是实际操作中,如StartServer函数,其只是返回了一个状态为STARTED的状态对象,并无其他操作。既然是返回状态,也有可能是其他处需要此状态,但是通过grep搜索StartServer也并未发现有按期望的调用。在此歇笔,若后文其他模块发现这两个操作用法的意图,则会补明。【???】

Admin服务关于模块日志的函数中,涉及到了flogging,在fabirc源码解析3中已经详述,可参看之后再追溯Admin的这三个函数。若对fabric的日志系统有所了解,则此处的三个函数的作用很好理解。以SetModuleLogLevel为例,作用就是从客户端获取requst中包含的模块关键字和要设置的日志级别,然后在flogging中进行设置。

Endorser

Endorser,个人习惯直译成背书者。类似于对支票的背书,表示对一种行为或权利的认可。放在fabric中,就是一个主体接收到其他某一peer点发送的申请消息,通过检查该申请消息中的签名的方式,向发送者表示支持和认可,这样该请求或申请才有可能被最终提交,使之作用于系统。这里说有可能,是指一个申请可能还需要满足一些条件,比如必须得到一定比例数量的背书,才算得到整个系统的认同。而这些条件,就是指的背书策略

背书者Endorser在一个交易流中充当的作用如下:

  1. 客户端发送一个背书申请(SignedProposal)到Endorser。
  2. Endorser对申请进行背书,发送一个申请应答(ProposalResponse)到客户端。
  3. 客户端将申请应答中的背书组装到一个交易请求(SignedTransaction)中。

在此简单提一下交易(Transaction)的概念,之后还会在主题文章中详述。交易是一个更大的概念,fabric的任何操作,如chaincode操作,甚至是配置系统的操作,都被定义为一项交易。而背书只是组成交易请求数据的一个准备环节而已,交易请求数据准备就绪后,会被发送到orderer。需要注意的是,一般交易和chaincode交易所进行的背书过程一致,但是背书过程所交流的数据中包含的内容项有所区别。具体可参看/fabric/protos/peer/proposal.proto和proposal_response.proto中的注释和数据原型定义。

背书服务原型和实现

Endorser服务的原型定义在/fabric/protos/peer/peer.proto以及对应生成的peer.pb.go中,核心代码实现在/fabric/core/endorser下。

//proto中定义的服务原型
//在/fabric/protos/peer/peer.proto中定义
service Endorser {
    rpc ProcessProposal(SignedProposal) returns (ProposalResponse) {}
}
//proto生成的服务端接口和注册函数
//在/fabric/protos/peer/peer.pb.go中定义
type EndorserServer interface {
    ProcessProposal(context.Context, *SignedProposal) (*ProposalResponse, error)
}
func RegisterEndorserServer(s *grpc.Server, srv EndorserServer) {
    s.RegisterService(&_Endorser_serviceDesc, srv)
}
//核心代码实现Endorser服务
type Endorser struct {
    policyChecker policy.PolicyChecker
}
//Endorser专用初始化函数
func NewEndorserServer() pb.EndorserServer {
    e := new(Endorser)
    e.policyChecker = policy.NewPolicyChecker(
        peer.NewChannelPolicyManagerGetter(),
        mgmt.GetLocalMSP(),
        mgmt.NewLocalMSPPrincipalGetter(),
    )
    return e
}
//实现ProcessProposal服务
func (e *Endorser) ProcessProposal(...){...}
func (e *Endorser) endorseProposal(...){...}
...

Endorser服务的核心实现中,只有一个核心函数ProcessProposal,endorser.go中其余的函数都是相互配合供ProcessProposal调用,处理客户端发来的SignedProposal数据,返回ProposalResponse数据,完成最终的任务。

SignedProposal数据结构如下:

SignedProposal.png

ProposalResponse数据结构如下:

ProposalResponse.png

按图索骥,我们接下来顺着ProcessProposal函数来看看Endorser函数到底是如何处理从客户端传来的SignedProposal数据,然后包装何种数据到ProposalResponse并将之返回。

//ProcessProposal函数中调用
prop, hdr, hdrExt, err := validation.ValidateProposalMessage(signedProp)

首先,第一步,ProcessProposal函数使用ValidateProposalMessage对所接收的signedProp数据进行了验证,并返回signedProp中的一些字段Unmarshal过后的数据。这个函数还是比较绕的,而且写在此处不是太顺理成章,因为涉及到数据验证,所以最起码要知道接收的都有哪些数据或哪种数据是合法的,你才能验证,而这又涉及到了客户端的操作,我们还没讲到。在将来讲到客户端相关操作的时候,可以于此对看。

验证过程涉及到了MSP。MSP将在相关主题文章中详述,在此撇开一笔简单介绍:MSP又是一个比较大的概念,是Membership Service Provider的缩写,个人习惯直译为成员关系服务提供者。类似于一个运行的fabric系统网络中,有众多的参与者,而MSP就是为了管理这些参与者,辨识验证哪些人有资格,哪些人没资格,既维护某一个参与者的权限,也维护参与者之间的关系。

回到本文主线,ValidateProposalMessage在/fabric/core/common/validation/msgvalidation.go中定义,调用了同文件中的辅助验证函数和/fabric/protos/utils下的工具函数(putils.XXX一类的函数,下文用putils表示)对SignedProposal按结构进行逐步验证。/fabric/protos/utils下的工具函数基本都是对protos中所定义的数据原型的Unmarshal操作,如proputils.go中的GetProposal函数,就是尝试着将传入的[]byte格式的数据Unmarshal成/fabric/protos/peer/proposal.pb.go中定义的Proposal结构数据。

//ValidateProposalMessage函数中调用
//与ValidateProposalMessage函数在同文件中
chdr, shdr, err := validateCommonHeader(hdr)
err = checkSignatureFromCreator(shdr.Creator, ...)
err = utils.CheckProposalTxID(...)
switch common.HeaderType(chdr.Type) {
    case ...
    case common.HeaderType_ENDORSER_TRANSACTION:
        chaincodeHdrExt, err := validateChaincodeProposalMessage(prop, hdr)
    default:
    ...
}

参看结构图,validateCommonHeader函数验证了SignedProposal中的ProposalBytes中的Header,主要验证的SignatureHeader中的Creator不为空、Nonce不为空且存在,ChannelHeader中的Type必须是ENDORSER_TRANSACTION、CONFIG_UPDATE、CONFIG三者之一,并顺道返回了Unmarshal出来的对应的结构体数据对象。

checkSignatureFromCreator函数利用validateCommonHeader顺道返回的结构体数据,验证了SignedProposal中的证书、背书策略,发起者的身份等的有效性,是核心验证函数。这里面大有文章,也使用到了MSP,进而使用到了bccsp,但也正因这样,将此细节放入MSP服务主题文章中详述。

CheckProposalTxID函数将SignatureHeader中由Nonce和Creator联合生成的Hash字符串值与ChannelHeader中的TxId做对比,这也同时说明TxId就是Nonce和Creator联合生成的Hash值。这里的Nonce是为了防止replay attack,在加密技术中经常用到。

validateChaincodeProposalMessage函数简单验证了Proposal中的Extension对应的结构体ChaincodeHeaderExtension中的PayloadVisibility是否为空。该字段控制着Proposal的payload在最终的交易和在ledger中能用到的范围。至此,对接收的SignedProposal对象signedProp验证完毕,并返回SignedProposal中的Proposal,Proposal中的Header和ChaincodeHeaderExtension字段,分别为prop,hdr,hdrExt。对SignedProposal的验证内容,在/fabric/protos/peer/proposal.proto中定义SignedProposal原型时,就注释了4点,可以阅读参考。

//ProcessProposal函数中调用
chdr, err := putils.UnmarshalChannelHeader(hdr.ChannelHeader)
shdr, err := putils.GetSignatureHeader(hdr.SignatureHeader)
if syscc.IsSysCCAndNotInvokableExternal(hdrExt.ChaincodeId.Name){...}

紧接着,第二步,在ProcessProposal函数中,因为validation.ValidateProposalMessage没有返回,所有再次使用putils下的函数获取Header下的ChannelHeader和SignatureHeader,分别为chdr,shdr。然后用函数IsSysCCAndNotInvokableExternal验证目前所处理的SignedProposal所涉及的chaincode的ID是否是系统chaincode,若是,是否能被外部调用。如果该chaincodeID是系统chaincode且不能被外部调用,则返回true,则进入if分支,返回相应错误。这表明,发送SignedProposal所在的chaincode,若是系统chaincode时,要保证该系统chaincode允许被外界调用。在系统chaincode中,InvokableExternal字段设定了是否可以被外界调用。

//频道ID
chainID := chdr.ChannelId
//交易ID
txid := chdr.TxId
if chainID != "" {
    lgr := peer.GetLedger(chainID)
    if _, err := lgr.GetTransactionByID(txid); err == nil { return }
    if !syscc.IsSysCC(hdrExt.ChaincodeId.Name) {
        if err = e.checkACL(signedProp, chdr, shdr, hdrExt);
        ...
    }
}else{
    //do nothing
}

第三步,ValidateProposalMessage的检查中并没有检查频道ID是否为空,当频道ID不为空时,程序根据频道ID调用GetLedger获取peer本地的账本PeerLedger,然后根据交易ID调用账本对象自身函数查看该交易ID是否已经存在于账本中,即交易的唯一性检查。接下来进行策略检查,当chaincode**不是系统chaincode时,会调用背书者成员policyChecker的函数checkACL,对背书者接收的SignedProposal是否符合所依赖频道的写者策略**(writers policy of the chain)进行检查(系统chaincode的检查在其他地方)。成员policyChecker在背书者创建的时候被赋予为policy.NewPolicyChecker(...)的值,详细的过程写到下文的背书策略中。如果频道ID为空,则什么都不做(当前版本是这样),注释是这么说的:因为交易忽略了唯一性检查,没有频道ID的proposal不会影响到ledger,也不会被提交(submitted)。没有频道ID的proposal是对照peer的本地MSP验证有效的,而不是通过调用ValidateProposalMessage函数。

//交易模拟器接口
var txsim ledger.TxSimulator
//账本历史查询接口
var historyQueryExecutor ledger.HistoryQueryExecutor
if chainID != "" {
    //交易模拟器
    if txsim, err = e.getTxSimulator(chainID); err != nil {...}
    //账本历史查询器
    if historyQueryExecutor, err = e.getHistoryQueryExecutor(chainID); err != nil {...}
    ctx = context.WithValue(ctx, chaincode.HistoryQueryExecutorKey, historyQueryExecutor)
    defer txsim.Done()
}
//模拟交易
cd, res, simulationResult, ccevent, err := e.simulateProposal(...)

第四步,当频道ID不为空时,背书者对象使用自身的函数e.getTxSimulator(chainID)e.getHistoryQueryExecutor(chainID),根据频道ID分别获取了交易模拟对象和账本历史查询对象,进行模拟(simulate)交易。从这里可以看出,频道ID chainID 也被作为了这个频道的账本的名称,因为peer的账本是存在在/fabric/core/peer/peer.go中的chains映射中的,映射的key就是频道ID。

和上一步类似,也是通过频道ID获取账本对象,然后使用账本对象的接口NewTxSimulator得到交易模拟器。交易模拟器定义在/fabric/core/ledger/kvledger/txmgmt/txmgr/lockbasedtxmgr/lockbased_tx_simulator.go中,隶属于同目录下lockbased_txmgr.go中的交易管理者LockBasedTxMgr。使用账本对象的接口NewHistoryQueryExecutor得到账本历史查询器。历史查询器定义在fabric/core/ledger/kvledger/history/historydb/historyleveldb/historyleveldb_query_executer.go中,隶属于同目录下historyleveldb.go中的账本历史数据库historyDB。而且,程序中调用了context.WithValue将账本历史查询器对象添加到了context中(关于context,还请自行学习此标准库的用法)。

得到了模拟所需的对象(交易模拟器和账本历史查询器)后,背书者使用自己的函数simulateProposal模拟交易。这里的模拟,不是说chaincode没有真正的执行,而应该理解是为对现实交易的模拟的意思,即,chaincode中所谓的智能合约的部分被确确实实的执行并产生了相应的结果集合,只不过这个过程是用数字化模拟出来的。simulateProposal中使用了背书者对象的checkEsccAndVsccgetCDSFromLSCCcallChaincode三个内调函数和交易模拟器的GetTxSimulationResults完成模拟任务。

(1)checkEsccAndVscc在目前版本里直接返回nil了,自带TODO标签,在此不延伸。(2)getCDSFromLSCC,若前面获得的交易模拟器不为空,则将其也加入了context。然后开始调用chaincode关于执行的代码,这里是从LSCC中获取指定名字的chaincode的数据。chaincode关于执行的代码在/fabric/core/chaincode中chaincodeexec.go和exectransaction.go中,最终完成核心任务的是exectransaction.go中的Execute函数,其中使用了ChaincodeSupport服务(服务支持各个peer之间的通信交流,这也就是所谓垫片的地位,核心逻辑由主题代码实现,而与各个peer之间通信去实现主题代码的功能,则用该服务支撑)。关于chaincode执行相关的代码,在此只是简单涉及,之后会在专题文章中详述。(3)callChaincode,真正执行了chaincode并返回HTTP状态应答和执行的chaincode事件。这里也是调用chaincode关于执行的代码,HTTP状态应答原型定义在/fabric/protos/commom/common.proto中。(4)GetTxSimulationResults,在lockbased_tx_simulator.go中定义,获取执行chaincode的读写集合。

至此,模拟交易函数simulateProposal执行完毕,返回chaincode数据、执行chaincode的应答信息、模拟结果集合、chaincode的执行事件,供下一步使用。这一步中,要注意当频道ID为空时,交易模拟器即为空,之后的相关操作也会有所区别。

var pResp *pb.ProposalResponse
if chainID == "" {
    pResp = &pb.ProposalResponse{Response: res}
}else{
    pResp, err = e.endorseProposal(...)
}
pResp.Response.Payload = res.Payload

最后,第五步,背书者对象使用自身函数endorseProposal对模拟交易进行背书,并得到交易申请应答数据pResp *pb.ProposalResponse。当频道ID为空时,简单的将上一步所得的应答信息赋予Response即返回,当频道ID不为空时,则使用上一步所返回的数据进行交易的背书。endorseProposal中主要调用的也是背书者的内调函数callChaincode,其之上是为了它准备数据,其之后是根据背书返回的数组组装申请应答信息ProposalResponse。模拟交易和背书都调用了callChaincode,而实现了不同的功能,主要起分别作用的是传入其的倒数第三个参数,该参数是chaincode的执行详细说明书ChaincodeInvocationSpec,说明书不同的内容能指导chaincode关于执行的代码实现不同的功能。如背书功能,使用的说明书中ChaincodeID指定的就是系统chaincode中用于背书的escc,而chaincode关于执行chaincode的代码也就根据所给定的ChaincodeID找到指定的chaincode执行。

Endorser对象的ProcessProposal进行的五步,可参看fabirc源码解析10——文档翻译之Architecture中的章节2部分。

频道中的策略检查器

fabirc源码解析7中的Handler对象和fabirc源码解析8中的lscc,qscc对象中,与此篇文章中的背书者Endorser都存在一个相同的成员policyChecker policy.PolicyChecker,且在初始化该对象的时候,都使用了同样的代码:

policyChecker = policy.NewPolicyChecker(
    peer.NewChannelPolicyManagerGetter(),
    mgmt.GetLocalMSP(),
    mgmt.NewLocalMSPPrincipalGetter(),
)

这就是所谓的策略检查器,而我们通过探寻这个检查器检查了什么,怎么检查的,可以顺带了解一下频道中的各种策略。

策略相关的代码集中在/fabric/core/policy和policyprovider/fabric/common/cauthdsl和policies/fabric/protos/common/policies.proto和对应生成的policies.pb.go

  • /fabric/protos/common/policies.proto:ImplicitMetaPolicy的原型定义。

  • /fabric/common/policies:定义了Policy接口、Manager接口和其实现ManagerImpl,定义了频道策略管理者获取器ChannelPolicyManagerGetter接口(其实现在/fabric/core/peer/peer.go中的channelPolicyManagerGetter)。对应定义了ImplicitMetaPolicy(也是一种类型的策略)。

  • fabric/common/cauthdsl:在policy.go中实现了Policy接口,定义和实现了策略对象提供者provider,在policyparser.go中实现了原始字符串策略(如”OR(‘A.member’, AND(‘B.member’, ‘C.member’))”)的解析(FromString函数),cauthdsl.go中实现了生成指定策略的评估函数,即策略的Evaluate接口(compile函数),cauthdsl_builder.go中则定义了用于生成各种所需结构的函数。

  • /fabric/core/policy/policy.go:定义了PolicyChecker的接口和其实现policyChecker,定义了策略检查器工厂PolicyCheckerFactory接口。

  • /fabric/core/policyprovider/provider.go:实现了PolicyCheckerFactory接口defaultFactory并初始化了一个实例对象。

策略接口定义如下:

//策略接口,在/fabric/common/policies/policy.go中
type Policy interface {
    //对比SignedData中的签名是否满足SignedData中策略
    Evaluate(signatureSet []*cb.SignedData) error
}
//错误或拒绝情况下的策略实现
type rejectPolicy string
func (rp rejectPolicy) Evaluate(signedData []*cb.SignedData) error {
    return fmt.Errorf("No such policy type: %s", rp)
}

//策略的实现1,在fabric/common/cauthdsl/policy.go中
type policy struct {
    evaluator func([]*cb.SignedData, []bool) bool
}
func (p *policy) Evaluate(signatureSet []*cb.SignedData) error { ... }

//策略的实现2,在/fabric/common/policiesimplicitmeta.go中
type implicitMetaPolicy struct {
    conf        *cb.ImplicitMetaPolicy
    threshold   int
    subPolicies []Policy
}
func (imp *implicitMetaPolicy) initialize(config *policyConfig) { ... }
func (imp *implicitMetaPolicy) Evaluate(...) error { ... }

回到策略检查器身上,策略检查器的定义如下:

//策略检查器接口
type PolicyChecker interface {
    CheckPolicy(...) error
    CheckPolicyBySignedData(...) error
    CheckPolicyNoChannel(...) error
}
//策略检查器实现
type policyChecker struct {
    //频道策略管理者获取器接口,在/fabric/peer/peer.go中实现
    channelPolicyManagerGetter policies.ChannelPolicyManagerGetter
    //本地MSP或MSPManager,在/fabric/msp中定义和实现
    localMSP  msp.IdentityDeserializer
    //MSP主角获取器,在/fabric/msp/mgmt/principal.go中定义和实现
    principalGetter  mgmt.MSPPrincipalGetter
}
func (p *policyChecker) CheckPolicy(...){ ... }
func (p *policyChecker) CheckPolicyNoChannel(...){ ... }
func (p *policyChecker) CheckPolicyBySignedData(){
    ...
    //获取策略管理者
    policyManager, _ := p.channelPolicyManagerGetter.Manager(channelID)
    //根据策略名获取策略对象,在/fabric/common/policies/policy.go中定义和实现
    policy, _ := policyManager.GetPolicy(policyName)
    //评定策略
    err := policy.Evaluate(sd)
    ...
}

首先是从哪里获取策略的问题,变相的我们就可以知道频道的策略都存储在哪儿了。在策略检查器的三个接口存在内部相互调用,最典型的就是CheckPolicyBySignedData接口,该接口中用三句代码完成了策略评定,如上代码中注释的那样。据此就可以很容易看出策略其实是存储在策略管理者对象policyManager中的config成员中,策略对象的名字是其所在的路径字符串,这些路径字符串在/fabric/common/policies/policy.go中开始的部分用常量定义,进而在此我们就可以看到都有哪些类型的策略:ChannelReaders、ChannelWriters、ChannelApplicationReaders等,对应的就是频道的读者策略,频道的写者策略,频道应用的读者策略等。根据GetPolicy的过程,我们甚至可以看出策略管理者是怎么编排其所管理的策略对象的,管理者与管理者存在父子关系,呈现的管理形式也和目录结构类似。

其次是如何验证策略的问题,不同的策略有不同的验证方法,所以策略检查器policyChecker有三个接口。一般的,直接调用所获取的策略对象的接口Evaluate进行评定,而在CheckPolicyNoChannel中则使用了MSP对象(即MSP或MSPManager)的Verify接口进行评定。

最后是验证了什么的问题,根据背书者Endorser使用policyChecker的地方,在checkACL内调函数中。而checkACL是在进行模拟交易simulateProposal之前被调用的(在ProcessProposal函数中),这也就是说policyChecker在背书过程中所做的就是检查一下客户端发来的请求数据是否合法,也就是检查的客户端的签名,也就是说是根据频道的写者策略判断客户端在该频道中是否有权利发送请求。因此,不要把这个策略检查器与验证是否满足背书策略的那个检查(这个是发生在模拟交易后的,由Evaluate完成)混淆。

猜你喜欢

转载自blog.csdn.net/idsuf698987/article/details/77044436
今日推荐