When the picture cannot be displayed, please check the original text: https://lessister.site/2019/10/29/fabric-transaction-endorser-source/
Article directory
- Technological process
- Source code analysis
- Proposal definition
- gRPC definition
- SDK send Proposal
- Peer receives Proposal
- Peer processing Proposal main process
- preProcess check and get information
- Endorsement node simulates transaction execution
- Get simulator
- Simulated execution
- endorser section
- chaincode section
- Get chain code execution environment
- Simulated execution transaction
- Process chain code container simulation response
- Release simulator resources
- ESCC processes simulation execution results
- Send Response
- summary
Technological process
stay Getting started with Fabric core concepts and frameworks This article introduces many concepts of Fabric, including transaction, Proposal and chain code. At the same time, it also introduces the execution process of transaction, the call process of chain code, etc.
This paper focuses on one part of the transaction process: transaction endorsement, the following three figures, in Getting started with Fabric core concepts and frameworks There are introductions in. If necessary, read the context information.
Macro transaction process
Please read the detailed process of the transaction Transaction process , to understand several aspects of the transaction process.
Chain code calling process
The above figure shows the endorsement process of the three main bodies of client, Peer and chain code container in the transaction process. Please pay attention to the Handler in Peer, which is responsible for interaction with chain code container.
Proposal endorsement process
[failed to transfer the pictures in the external link. The source station may have anti-theft chain mechanism. It is recommended to save the pictures and upload them directly (img-jcwq1ibp-1584264381251) (http://img.lessister.site/2019-07-chaincode_swimlane. PNG))
The figure above shows the process of transaction endorsement from the level close to the source code. Among them, Fabric and Shim are modules in Peer, ChainCode represents chain code container, and Endorser Chaincode represents Peer to endorse transaction proposal and simulation execution results.
If you know about Chaincode, you will know that Shim is the module on which the chain code container and Peer interact.
Finally, I would like to recommend a copy prepared by Baohua Peer proposal endorsement process Before reading the source code, you must read the data. Although it is streamlined, it has connected the important core processes.
Source code analysis
Proposal definition
The client sends the endorsed node is SignedProposal, which includes signature and Proposal. This is an introduction to its composition in proposal.proto
SignedProposal |\_ Signature (signature on the Proposal message by the creator specified in the header) \_ Proposal |\_ Header (the header for this proposal) \_ Payload (the payload for this proposal)
The file of proposal.proto also briefly introduces the message type and process of communication between Client and endorsement node.
Proposal
type SignedProposal struct { // The bytes of Proposal ProposalBytes []byte `protobuf:"bytes,1,opt,name=proposal_bytes,json=proposalBytes,proto3" json:"proposal_bytes,omitempty"` // Signaure over proposalBytes; this signature is to be verified against // the creator identity contained in the header of the Proposal message // marshaled as proposalBytes Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` }
type Proposal struct { // The header of the proposal. It is the bytes of the Header Header []byte `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"` // The payload of the proposal as defined by the type in the proposal // header. Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` // Optional extensions to the proposal. Its content depends on the Header's // type field. For the type CHAINCODE, it might be the bytes of a // ChaincodeAction message. Extension []byte `protobuf:"bytes,3,opt,name=extension,proto3" json:"extension,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` }
Header
type Header struct { ChannelHeader []byte `protobuf:"bytes,1,opt,name=channel_header,json=channelHeader,proto3" json:"channel_header,omitempty"` SignatureHeader []byte `protobuf:"bytes,2,opt,name=signature_header,json=signatureHeader,proto3" json:"signature_header,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } // Header is a generic replay prevention and identity message to include in a signed payload type ChannelHeader struct { Type int32 `protobuf:"varint,1,opt,name=type,proto3" json:"type,omitempty"` // Version indicates message protocol version Version int32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` // Timestamp is the local time when the message was created // by the sender Timestamp *timestamp.Timestamp `protobuf:"bytes,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // Identifier of the channel this message is bound for ChannelId string `protobuf:"bytes,4,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` // An unique identifier that is used end-to-end. // - set by higher layers such as end user or SDK // - passed to the endorser (which will check for uniqueness) // - as the header is passed along unchanged, it will be // be retrieved by the committer (uniqueness check here as well) // - to be stored in the ledger TxId string `protobuf:"bytes,5,opt,name=tx_id,json=txId,proto3" json:"tx_id,omitempty"` // The epoch in which this header was generated, where epoch is defined based on block height // Epoch in which the response has been generated. This field identifies a // logical window of time. A proposal response is accepted by a peer only if // two conditions hold: // 1. the epoch specified in the message is the current epoch // 2. this message has been only seen once during this epoch (i.e. it hasn't // been replayed) Epoch uint64 `protobuf:"varint,6,opt,name=epoch,proto3" json:"epoch,omitempty"` // Extension that may be attached based on the header type Extension []byte `protobuf:"bytes,7,opt,name=extension,proto3" json:"extension,omitempty"` // If mutual TLS is employed, this represents // the hash of the client's TLS certificate TlsCertHash []byte `protobuf:"bytes,8,opt,name=tls_cert_hash,json=tlsCertHash,proto3" json:"tls_cert_hash,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } type SignatureHeader struct { // Creator of the message, a marshaled msp.SerializedIdentity Creator []byte `protobuf:"bytes,1,opt,name=creator,proto3" json:"creator,omitempty"` // Arbitrary number that may only be used once. Can be used to detect replay attacks. Nonce []byte `protobuf:"bytes,2,opt,name=nonce,proto3" json:"nonce,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` }
gRPC definition
// EndorserClient is the client API for Endorser service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type EndorserClient interface { ProcessProposal(ctx context.Context, in *SignedProposal, opts ...grpc.CallOption) (*ProposalResponse, error) } // EndorserServer is the server API for Endorser service. type EndorserServer interface { ProcessProposal(context.Context, *SignedProposal) (*ProposalResponse, error) }
SDK send Proposal
Peer receives Proposal
Peer processing Proposal main process
It is mainly to aggregate the endorsement work of the endorsement node as follows:
- Proposal preprocessing
- Get the transaction execution simulator to simulate the execution of Proposal
- If the simulation is executed successfully, ESCC is called to endorse the Proposal and result. If the simulation fails, the response of endorsement failure is returned directly
func (e *Endorser) ProcessProposal(ctx context.Context, signedProp *pb.SignedProposal) (*pb.ProposalResponse, error) { ... // 0 -- check and validate // There's a lot of work here 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 // Get designated ledger simulator // obtaining once the tx simulator for this proposal. This will be nil // for chainless proposals // Also obtain a history query executor for history queries, since tx simulator does not cover history 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, } // this could be a request to a chainless SysCC // Simulate the execution of the transaction. If it fails, the response of endorsement failure will be returned // 1 -- simulate 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 { if res.Status >= shim.ERROR { endorserLogger.Errorf("[%s][%s] simulateProposal() resulted in chaincode %s response status %d for txid: %s", chainID, shorttxid(txid), hdrExt.ChaincodeId, res.Status, txid) var cceventBytes []byte if ccevent != nil { cceventBytes, err = putils.GetBytesChaincodeEvent(ccevent) if err != nil { return nil, errors.Wrap(err, "failed to marshal event bytes") } } 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()}}, nil } return pResp, nil } } // Sign and endorse the results of simulation execution // 2 -- endorse and get a marshalled ProposalResponse message var pResp *pb.ProposalResponse // TODO till we implement global ESCC, CSCC for system chaincodes // chainless proposals (such as CSCC) don't have to be endorsed if chainID == "" { pResp = &pb.ProposalResponse{Response: res} } else { // Note: To endorseProposal(), we pass the released txsim. Hence, an error would occur if we try to use this txsim pResp, err = e.endorseProposal(ctx, chainID, txid, signedProp, prop, res, simulationResult, ccevent, hdrExt.PayloadVisibility, hdrExt.ChaincodeId, txsim, cd) ... } // Set the proposal response payload - it // contains the "return value" from the // chaincode invocation pResp.Response = res // total failed proposals = ProposalsReceived-SuccessfulProposals e.Metrics.SuccessfulProposals.Add(1) success = true return pResp, nil }
preProcess check and get information
// preProcess checks the tx proposal headers, uniqueness and ACL // Check proposal and ACL func (e *Endorser) preProcess(signedProp *pb.SignedProposal) (*validateResult, error) { // Including proposal, header, chainID, txid and other information vr := &validateResult{} // at first, we check whether the message is valid // Check proposal and get all kinds of information you need 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 } // Get 2 headers in the Header chdr, err := putils.UnmarshalChannelHeader(hdr.ChannelHeader) if err != nil { vr.resp = &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}} return vr, err } shdr, err := putils.GetSignatureHeader(hdr.SignatureHeader) if err != nil { vr.resp = &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}} return vr, err } // Check whether the non external (user) system chain code is called // First, find the chain code instance, then call the chain code to determine whether it can be invoked. // block invocations to security-sensitive system chaincodes 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) vr.resp = &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}} return vr, err } chainID := chdr.ChannelId txid := chdr.TxId endorserLogger.Debugf("[%s][%s] processing txid: %s", chainID, shorttxid(txid), txid) if chainID != "" { // labels that provide context for failure metrics meterLabels := []string{ "channel", chainID, "chaincode", hdrExt.ChaincodeId.Name + ":" + hdrExt.ChaincodeId.Version, } // Check whether the transaction has been linked // Here we handle uniqueness check and ACLs for proposals targeting a chain // Notice that ValidateProposalMessage has already verified that TxID is computed properly if _, err = e.s.GetTransactionByID(chainID, txid); err == nil { // increment failure due to duplicate transactions. Useful for catching replay attacks in // addition to benign retries e.Metrics.DuplicateTxsFailure.With(meterLabels...).Add(1) err = errors.Errorf("duplicate transaction found [%s]. Creator [%x]", txid, shdr.Creator) vr.resp = &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}} return vr, err } // User chain code check ACL // check ACL only for application chaincodes; ACLs // for system chaincodes are checked elsewhere if !e.s.IsSysCC(hdrExt.ChaincodeId.Name) { // check that the proposal complies with the Channel's writers if err = e.s.CheckACL(signedProp, chdr, shdr, hdrExt); err != nil { e.Metrics.ProposalACLCheckFailed.With(meterLabels...).Add(1) vr.resp = &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}} return vr, err } } } else { // chainless proposals do not/cannot affect ledger and cannot be submitted as transactions // ignore uniqueness checks; also, chainless proposals are not validated using the policies // of the chain since by definition there is no chain; they are validated against the local // MSP of the peer instead by the call to ValidateProposalMessage above } // Save extracted information vr.prop, vr.hdrExt, vr.chainID, vr.txid = prop, hdrExt, chainID, txid return vr, nil } // ValidateProposalMessage checks the validity of a SignedProposal message // this function returns Header and ChaincodeHeaderExtension messages since they // have been unmarshalled and validated func ValidateProposalMessage(signedProp *pb.SignedProposal) (*pb.Proposal, *common.Header, *pb.ChaincodeHeaderExtension, error) { if signedProp == nil { return nil, nil, nil, errors.New("nil arguments") } putilsLogger.Debugf("ValidateProposalMessage starts for signed proposal %p", signedProp) // extract the Proposal message from signedProp prop, err := utils.GetProposal(signedProp.ProposalBytes) if err != nil { return nil, nil, nil, err } // 1) look at the ProposalHeader hdr, err := utils.GetHeader(prop.Header) if err != nil { return nil, nil, nil, err } // validate the header chdr, shdr, err := validateCommonHeader(hdr) if err != nil { return nil, nil, nil, err } // Signature from SignatureHeader transaction client // validate the signature err = checkSignatureFromCreator(shdr.Creator, signedProp.Signature, signedProp.ProposalBytes, chdr.ChannelId) if err != nil { // log the exact message on the peer but return a generic error message to // avoid malicious users scanning for channels putilsLogger.Warningf("channel [%s]: %s", chdr.ChannelId, err) sId := &msp.SerializedIdentity{} err := proto.Unmarshal(shdr.Creator, sId) if err != nil { // log the error here as well but still only return the generic error err = errors.Wrap(err, "could not deserialize a SerializedIdentity") putilsLogger.Warningf("channel [%s]: %s", chdr.ChannelId, err) } return nil, nil, nil, errors.Errorf("access denied: channel [%s] creator org [%s]", chdr.ChannelId, sId.Mspid) } // Check whether the calculation of txid complies with the rules // Verify that the transaction ID has been computed properly. // This check is needed to ensure that the lookup into the ledger // for the same TxID catches duplicates. err = utils.CheckTxID( chdr.TxId, shdr.Nonce, shdr.Creator) if err != nil { return nil, nil, nil, err } // Check the proposal according to different types // continue the validation in a way that depends on the type specified in the header switch common.HeaderType(chdr.Type) { case common.HeaderType_CONFIG: //which the types are different the validation is the same //viz, validate a proposal to a chaincode. If we need other //special validation for confguration, we would have to implement //special validation fallthrough case common.HeaderType_ENDORSER_TRANSACTION: // Mainly extract the ChaincodeHeaderExtension // validation of the proposal message knowing it's of type CHAINCODE chaincodeHdrExt, err := validateChaincodeProposalMessage(prop, hdr) if err != nil { return nil, nil, nil, err } return prop, hdr, chaincodeHdrExt, err default: //NOTE : we proably need a case return nil, nil, nil, errors.Errorf("unsupported proposal type %d", common.HeaderType(chdr.Type)) } }
Endorsement node simulates transaction execution
Get simulator
// Support contains functions that the endorser requires to execute its tasks type Support interface { crypto.SignerSupport // IsSysCCAndNotInvokableExternal returns true if the supplied chaincode is // ia system chaincode and it NOT invokable IsSysCCAndNotInvokableExternal(name string) bool // GetTxSimulator returns the transaction simulator for the specified ledger // a client may obtain more than one such simulator; they are made unique // by way of the supplied txid GetTxSimulator(ledgername string, txid string) (ledger.TxSimulator, error) } // GetTxSimulator returns the transaction simulator for the specified ledger // a client may obtain more than one such simulator; they are made unique // by way of the supplied txid func (s *SupportImpl) GetTxSimulator(ledgername string, txid string) (ledger.TxSimulator, error) { // Create simulators using ledger and txid, with separate simulators for each transaction lgr := s.Peer.GetLedger(ledgername) if lgr == nil { return nil, errors.Errorf("Channel does not exist: %s", ledgername) } return lgr.NewTxSimulator(txid) } // NewTxSimulator returns new `ledger.TxSimulator` func (l *kvLedger) NewTxSimulator(txid string) (ledger.TxSimulator, error) { return l.txtmgmt.NewTxSimulator(txid) } // NewTxSimulator implements method in interface `txmgmt.TxMgr` func (txmgr *LockBasedTxMgr) NewTxSimulator(txid string) (ledger.TxSimulator, error) { logger.Debugf("constructing new tx simulator") s, err := newLockBasedTxSimulator(txmgr, txid) if err != nil { return nil, err } txmgr.commitRWLock.RLock() return s, nil } // There are two important ones: query executor and read / write set builder // LockBasedTxSimulator is a transaction simulator used in `LockBasedTxMgr` type lockBasedTxSimulator struct { lockBasedQueryExecutor rwsetBuilder *rwsetutil.RWSetBuilder writePerformed bool pvtdataQueriesPerformed bool simulationResultsComputed bool paginatedQueriesPerformed bool } func newLockBasedTxSimulator(txmgr *LockBasedTxMgr, txid string) (*lockBasedTxSimulator, error) { // Create a read-write set builder to help build a read-write set rwsetBuilder := rwsetutil.NewRWSetBuilder() helper := newQueryHelper(txmgr, rwsetBuilder) logger.Debugf("constructing new tx simulator txid = [%s]", txid) return &lockBasedTxSimulator{lockBasedQueryExecutor{helper, txid}, rwsetBuilder, false, false, false, false}, nil } // LockBasedQueryExecutor is a query executor used in `LockBasedTxMgr` // Read only, no write related operations type lockBasedQueryExecutor struct { helper *queryHelper txid string }
Simulated execution
endorser section
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() // History finder 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, // The simulator is here HistoryQueryExecutor: historyQueryExecutor, } // this could be a request to a chainless SysCC // TODO: if the proposal has an extension, it will be of type ChaincodeAction; // if it's present it means that no simulation is to be performed because // we're trying to emulate a submitting peer. On the other hand, we need // to validate the supplied action before endorsing it // Simulate the execution of the transaction. If it fails, the response of endorsement failure will be returned // 1 -- simulate cd, res, simulationResult, ccevent, err := e.SimulateProposal(txParams, hdrExt.ChaincodeId)
Call the chain code module to simulate the execution of the transaction, obtain the public and private data read-write sets of the transaction execution, as well as the events generated by the transaction execution, and return the results to the upper layer for endorsement.
It also includes the processing of private data, which will be taken out and then spread to the Peer node in the private data through the mission.
// SimulateProposal simulates the proposal by calling the chaincode func (e *Endorser) SimulateProposal(txParams *ccprovider.TransactionParams, cid *pb.ChaincodeID) (ccprovider.ChaincodeDefinition, *pb.Response, []byte, *pb.ChaincodeEvent, error) { endorserLogger.Debugf("[%s][%s] Entry chaincode: %s", txParams.ChannelID, shorttxid(txParams.TxID), cid) defer endorserLogger.Debugf("[%s][%s] Exit", txParams.ChannelID, shorttxid(txParams.TxID)) // we do expect the payload to be a ChaincodeInvocationSpec // if we are supporting other payloads in future, this be glaringly point // as something that should change // Generate the information required for Invoke according to the Proposal cis, err := putils.GetChaincodeInvocationSpec(txParams.Proposal) if err != nil { return nil, nil, nil, nil, err } // Metadata of chain code var cdLedger ccprovider.ChaincodeDefinition var version string // Set version if !e.s.IsSysCC(cid.Name) { // Get the metadata of the chain code from lscc according to the chain code name to be called 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() // Actually driven, not realized err = e.s.CheckInstantiationPolicy(cid.Name, version, cdLedger) if err != nil { return nil, nil, nil, nil, err } } else { // scc version is fixed "latest" version = util.GetSysCCVersion() } // ---3. execute the proposal and get simulation results var simResult *ledger.TxSimulationResults var pubSimResBytes []byte var res *pb.Response var ccevent *pb.ChaincodeEvent // Simulation execution, the execution result is saved in the simulator 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 { // Obtain the simulation execution results through the simulator, including two read-write sets of public and private data if simResult, err = txParams.TXSimulator.GetTxSimulationResults(); err != nil { txParams.TXSimulator.Done() return nil, nil, nil, nil, err } // Private data exists if simResult.PvtSimulationResults != nil { if cid.Name == "lscc" { // TODO: remove once we can store collection configuration outside of LSCC txParams.TXSimulator.Done() return nil, nil, nil, nil, errors.New("Private data is forbidden to be used in instantiate") } // Get private data to be propagated through the mission pvtDataWithConfig, err := e.AssemblePvtRWSet(simResult.PvtSimulationResults, txParams.TXSimulator) // To read collection config need to read collection updates before // releasing the lock, hence txParams.TXSimulator.Done() moved down here txParams.TXSimulator.Done() if err != nil { return nil, nil, nil, nil, errors.WithMessage(err, "failed to obtain collections config") } endorsedAt, err := e.s.GetLedgerHeight(txParams.ChannelID) if err != nil { return nil, nil, nil, nil, errors.WithMessage(err, fmt.Sprint("failed to obtain ledger height for channel", txParams.ChannelID)) } // Add ledger height at which transaction was endorsed, // `endorsedAt` is obtained from the block storage and at times this could be 'endorsement Height + 1'. // However, since we use this height only to select the configuration (3rd parameter in distributePrivateData) and // manage transient store purge for orphaned private writesets (4th parameter in distributePrivateData), this works for now. // Ideally, ledger should add support in the simulator as a first class function `GetHeight()`. pvtDataWithConfig.EndorsedAt = endorsedAt // Send the private data with the channel id, transaction id and block height, representing the block and transaction to which the private data belongs if err := e.distributePrivateData(txParams.ChannelID, txParams.TxID, pvtDataWithConfig, endorsedAt); err != nil { return nil, nil, nil, nil, err } } // Transaction simulation completed, release the resources occupied by the simulator txParams.TXSimulator.Done() // Get public results of simulation execution if pubSimResBytes, err = simResult.GetPubSimulationBytes(); err != nil { return nil, nil, nil, nil, err } } // Return chain symbol data, simulation execution results, events generated by transaction execution return cdLedger, res, pubSimResBytes, ccevent, nil }
callChaincode calls the chaincode module to Execute the chain code. In the previous process, there is no distinction between system chain code SCC and user chain code UCC. Both SCC and UCC will be executed by passing the Execute function to the chain code module.
If you call lscc to deploy or upgrade UCC, ExecuteLegacyInit will be called to initialize the chain code container.
Finally, the chain code simulation results and events are returned.
// call specified chaincode (system or user) func (e *Endorser) callChaincode(txParams *ccprovider.TransactionParams, version string, input *pb.ChaincodeInput, cid *pb.ChaincodeID) (*pb.Response, *pb.ChaincodeEvent, error) { ... // scc is also running here // is this a system chaincode res, ccevent, err = e.s.Execute(txParams, txParams.ChannelID, cid.Name, version, txParams.TxID, txParams.SignedProp, txParams.Proposal, input) if err != nil { return nil, nil, err } ... // If you call lscc to deploy or upgrade the chain code, this process will be followed if cid.Name == "lscc" && len(input.Args) >= 3 && (string(input.Args[0]) == "deploy" || string(input.Args[0]) == "upgrade") { userCDS, err := putils.GetChaincodeDeploymentSpec(input.Args[2], e.PlatformRegistry) ... // Initialize the chain code container, and finally call the Init function of the chain code _, _, err = e.s.ExecuteLegacyInit(txParams, txParams.ChannelID, cds.ChaincodeSpec.ChaincodeId.Name, cds.ChaincodeSpec.ChaincodeId.Version, txParams.TxID, txParams.SignedProp, txParams.Proposal, cds) ... } // ----- END ------- return res, ccevent, err }
The Support interface actually integrates the external module functions required by many endorsement nodes, such as chain code, system chain code, ACL, etc.
// Support contains functions that the endorser requires to execute its tasks type Support interface // SupportImpl provides an implementation of the endorser.Support interface // issuing calls to various static methods of the peer type SupportImpl struct { *PluginEndorser crypto.SignerSupport Peer peer.Operations PeerSupport peer.Support ChaincodeSupport *chaincode.ChaincodeSupport SysCCProvider *scc.Provider ACLProvider aclmgmt.ACLProvider }
Execute is implemented by calling ChaincodeSupport.Execute.
// Execute a proposal and return the chaincode response func (s *SupportImpl) Execute(txParams *ccprovider.TransactionParams, cid, name, version, txid string, signedProp *pb.SignedProposal, prop *pb.Proposal, input *pb.ChaincodeInput) (*pb.Response, *pb.ChaincodeEvent, error) { cccid := &ccprovider.CCContext{ Name: name, Version: version, } // decorate the chaincode input decorators := library.InitRegistry(library.Config{}).Lookup(library.Decoration).([]decoration.Decorator) input.Decorations = make(map[string][]byte) input = decoration.Apply(prop, input, decorators...) txParams.ProposalDecorations = input.Decorations return s.ChaincodeSupport.Execute(txParams, cccid, input) }
chaincode section
Through the above interface, step into the door of the chain code module.
func (cs *ChaincodeSupport) Execute(txParams *ccprovider.TransactionParams, cccid *ccprovider.CCContext, input *pb.ChaincodeInput) (*pb.Response, *pb.ChaincodeEvent, error) { // Invoke gets ChaincodeMessage resp, err := cs.Invoke(txParams, cccid, input) // Get Response and event according to ChaincodeMessage return processChaincodeExecutionResult(txParams.TxID, cccid.Name, resp, err) } func (cs *ChaincodeSupport) Invoke(txParams *ccprovider.TransactionParams, cccid *ccprovider.CCContext, input *pb.ChaincodeInput) (*pb.ChaincodeMessage, error) { h, err := cs.Launch(txParams.ChannelID, cccid.Name, cccid.Version, txParams.TXSimulator) if err != nil { return nil, err } // Execute the transaction calling the chain code (the message between the chain code and the chain code is chaincodemessage? Transaction) cctype := pb.ChaincodeMessage_TRANSACTION return cs.execute(cctype, txParams, cccid, input, h) }
Get chain code execution environment
Launch can obtain the chain code execution environment, that is, the user chain code container. If the chain code that has been instantiated is not started on the current endorsement node, launch will return an interaction Handler with the chain code container.
Multiple chain code containers can be deployed on a Peer. In order to interact / communicate with these chain code containers, Peer creates a Handler for each chain code container. The Handler carries the resources for the interaction between the Peer and the chain code container.
// Launch starts executing chaincode if it is not already running. This method // blocks until the peer side handler gets into ready state or encounters a fatal // error. If the chaincode is already running, it simply returns. func (cs *ChaincodeSupport) Launch(chainID, chaincodeName, chaincodeVersion string, qe ledger.QueryExecutor) (*Handler, error) { cname := chaincodeName + ":" + chaincodeVersion if h := cs.HandlerRegistry.Handler(cname); h != nil { return h, nil } // Start chain container h := cs.HandlerRegistry.Handler(cname) if h == nil { return nil, errors.Wrapf(err, "[channel %s] claimed to start chaincode container for %s but could not find handler", chainID, cname) } return h, nil }
The starting process of the chain code container is not the focus of this article, so we will not go into the call details of Launch.
Simulated execution transaction
Execute encapsulates the message to execute the transaction, and then uses the Handler to execute the transaction.
// execute executes a transaction and waits for it to complete until a timeout value. func (cs *ChaincodeSupport) execute(cctyp pb.ChaincodeMessage_Type, txParams *ccprovider.TransactionParams, cccid *ccprovider.CCContext, input *pb.ChaincodeInput, h *Handler) (*pb.ChaincodeMessage, error) { input.Decorations = txParams.ProposalDecorations // Create message ccMsg, err := createCCMessage(cctyp, txParams.ChannelID, txParams.TxID, input) if err != nil { return nil, errors.WithMessage(err, "failed to create chaincode message") } // Execution of transactions ccresp, err := h.Execute(txParams, cccid, ccMsg, cs.ExecuteTimeout) if err != nil { return nil, errors.WithMessage(err, fmt.Sprintf("error sending")) } return ccresp, nil }
Note that the following parameter txParams *ccprovider.TransactionParams has the following type definitions. It contains information and resources during the execution of a transaction. Therefore, this parameter has always been in the process of transaction transmission.
// TransactionParams are parameters which are tied to a particular transaction // and which are required for invoking chaincode. type TransactionParams struct { TxID string ChannelID string SignedProp *pb.SignedProposal Proposal *pb.Proposal TXSimulator ledger.TxSimulator HistoryQueryExecutor ledger.HistoryQueryExecutor CollectionStore privdata.CollectionStore IsInitTransaction bool // this is additional data passed to the chaincode ProposalDecorations map[string][]byte }
The process of the Handler executing the transaction is as follows: create the Context context of the transaction execution. When the chain code container executes the transaction, it will communicate with the Peer many times to read and write the data. The Context can let the data read and write to get the correct information.
After that, the Handler sends the message to the chain code container and waits for the chain code container to send the message containing the execution result, or the execution times out. The default execution time is 30s.
func (h *Handler) Execute(txParams *ccprovider.TransactionParams, cccid *ccprovider.CCContext, msg *pb.ChaincodeMessage, timeout time.Duration) (*pb.ChaincodeMessage, error) { chaincodeLogger.Debugf("Entry") defer chaincodeLogger.Debugf("Exit") // Private data txParams.CollectionStore = h.getCollectionStore(msg.ChannelId) // Is chain code initialization performed txParams.IsInitTransaction = (msg.Type == pb.ChaincodeMessage_INIT) // Create transaction context txctx, err := h.TXContexts.Create(txParams) if err != nil { return nil, err } // Release the transaction context resource when exiting (transaction execution completed) defer h.TXContexts.Delete(msg.ChannelId, msg.Txid) // Save proposal to msg if err := h.setChaincodeProposal(txParams.SignedProp, txParams.Proposal, msg); err != nil { return nil, err } // Send msg to chain code container h.serialSendAsync(msg) // Wait for the chain code container to respond, or timeout var ccresp *pb.ChaincodeMessage select { case ccresp = <-txctx.ResponseNotifier: // response is sent to user or calling chaincode. ChaincodeMessage_ERROR // are typically treated as error case <-time.After(timeout): err = errors.New("timeout expired while executing transaction") ccName := cccid.Name + ":" + cccid.Version h.Metrics.ExecuteTimeouts.With( "chaincode", ccName, ).Add(1) } return ccresp, err }
Process chain code container simulation response
The Response of chain code container execution will be passed up until ChaincodeSupport.Execute. It calls processChaincodeExecutionResult to convert the Response returned by chain code container into the Response of transaction simulation execution, and the Response will finally be returned to the Endorser. You can call flow up.
func processChaincodeExecutionResult(txid, ccName string, resp *pb.ChaincodeMessage, err error) (*pb.Response, *pb.ChaincodeEvent, error) { if err != nil { return nil, nil, errors.Wrapf(err, "failed to execute transaction %s", txid) } if resp == nil { return nil, nil, errors.Errorf("nil response from transaction %s", txid) } if resp.ChaincodeEvent != nil { resp.ChaincodeEvent.ChaincodeId = ccName resp.ChaincodeEvent.TxId = txid } switch resp.Type { // If the transaction is executed successfully, the Response saved in the Payload will be extracted case pb.ChaincodeMessage_COMPLETED: res := &pb.Response{} err := proto.Unmarshal(resp.Payload, res) if err != nil { return nil, nil, errors.Wrapf(err, "failed to unmarshal response for transaction %s", txid) } return res, resp.ChaincodeEvent, nil // If it fails, extract the error information saved in the Payload case pb.ChaincodeMessage_ERROR: return nil, resp.ChaincodeEvent, errors.Errorf("transaction returned with failure: %s", resp.Payload) default: return nil, nil, errors.Errorf("unexpected response type %d for transaction %s", resp.Type, txid) } }
Release simulator resources
Recall that in Endorser.SimulateProposal, it obtains TXSimulator, the transaction simulation actuator. There are many resources in it. If it is not released in time, under high TPS, problems such as high Peer pressure, resource leakage and low performance will break out.
txParams.TXSimulator.Done() is used to release resources. As mentioned above, TxSimulator includes queryexecution, and lockbasedqueryexecution implements queryexecution. That is to say, it mainly releases resources related to query operations.
It can be seen from the source code that read-write locks and iterator resources will be released. If they are not released in time, the consequences will be terrible.
// Done implements method in interface `ledger.QueryExecutor` func (q *lockBasedQueryExecutor) Done() { logger.Debugf("Done with transaction simulation / query execution [%s]", q.txid) q.helper.done() } func (h *queryHelper) done() { if h.doneInvoked { return } defer func() { // Release lock h.txmgr.commitRWLock.RUnlock() h.doneInvoked = true // Release iterator for _, itr := range h.itrs { itr.Close() } }() }
ESCC processes simulation execution results
As mentioned above, the Response of simulated execution will eventually return to the Endorser, which will call ESCC to endorse the result and finally generate a ProposalResponse. Let's take a look at this process.
func (e *Endorser) ProcessProposal(ctx context.Context, signedProp *pb.SignedProposal) (*pb.ProposalResponse, error) { // Pre-process, simulate if chainID == "" { pResp = &pb.ProposalResponse{Response: res} } else { // Note: To endorseProposal(), we pass the released txsim. Hence, an error would occur if we try to use this txsim pResp, err = e.endorseProposal(ctx, chainID, txid, signedProp, prop, res, simulationResult, ccevent, hdrExt.PayloadVisibility, hdrExt.ChaincodeId, txsim, cd) // ... } // ... }
Endorser.endorseProposal
Endorsement chain code is pluggable, and different ESCC can be used. The endorsement process of system chain code and user chain code is different.
// endorse the proposal by calling the ESCC func (e *Endorser) endorseProposal(_ context.Context, chainID string, txid string, signedProp *pb.SignedProposal, proposal *pb.Proposal, response *pb.Response, simRes []byte, event *pb.ChaincodeEvent, visibility []byte, ccid *pb.ChaincodeID, txsim ledger.TxSimulator, cd ccprovider.ChaincodeDefinition) (*pb.ProposalResponse, error) { endorserLogger.Debugf("[%s][%s] Entry chaincode: %s", chainID, shorttxid(txid), ccid) defer endorserLogger.Debugf("[%s][%s] Exit", chainID, shorttxid(txid)) // System chain code and user chain code use different ESCC isSysCC := cd == nil // 1) extract the name of the escc that is requested to endorse this chaincode var escc string // ie, "lscc" or system chaincodes if isSysCC { escc = "escc" } else { escc = cd.Endorsement() } endorserLogger.Debugf("[%s][%s] escc for chaincode %s is %s", chainID, shorttxid(txid), ccid, escc) // marshalling event bytes var err error var eventBytes []byte if event != nil { eventBytes, err = putils.GetBytesChaincodeEvent(event) if err != nil { return nil, errors.Wrap(err, "failed to marshal event bytes") } } // set version of executing chaincode if isSysCC { // if we want to allow mixed fabric levels we should // set syscc version to "" ccid.Version = util.GetSysCCVersion() } else { ccid.Version = cd.CCVersion() } // Create endorsement context information ctx := Context{ PluginName: escc, // Plug-in name Channel: chainID, SignedProposal: signedProp, ChaincodeID: ccid, Event: eventBytes, SimRes: simRes, Response: response, Visibility: visibility, Proposal: proposal, TxID: txid, } // Call plug-in endorsement return e.s.EndorseWithPlugin(ctx) }
The endorsement plug-in implements the following interface.
// Plugin endorses a proposal response type Plugin interface { // Endorse signs the given payload(ProposalResponsePayload bytes), and optionally mutates it. // Returns: // The Endorsement: A signature over the payload, and an identity that is used to verify the signature // The payload that was given as input (could be modified within this function) // Or error on failure Endorse(payload []byte, sp *peer.SignedProposal) (*peer.Endorsement, []byte, error) // Init injects dependencies into the instance of the Plugin Init(dependencies ...Dependency) error }
To use plug-in endorsement, you need to obtain the plug-in instance, and then assemble the response Payload, which contains a variety of results of transaction execution, and then endorse the Payload and the signed Proposal.
// EndorseWithPlugin endorses the response with a plugin func (pe *PluginEndorser) EndorseWithPlugin(ctx Context) (*pb.ProposalResponse, error) { endorserLogger.Debug("Entering endorsement for", ctx) if ctx.Response == nil { return nil, errors.New("response is nil") } if ctx.Response.Status >= shim.ERRORTHRESHOLD { return &pb.ProposalResponse{Response: ctx.Response}, nil } // Get plug-ins plugin, err := pe.getOrCreatePlugin(PluginName(ctx.PluginName), ctx.Channel) if err != nil { endorserLogger.Warning("Endorsement with plugin for", ctx, " failed:", err) return nil, errors.Errorf("plugin with name %s could not be used: %v", ctx.PluginName, err) } // Compose the information of simulation execution to generate endorsement response Payload prpBytes, err := proposalResponsePayloadFromContext(ctx) if err != nil { endorserLogger.Warning("Endorsement with plugin for", ctx, " failed:", err) return nil, errors.Wrap(err, "failed assembling proposal response payload") } // Endorse Payload and signed Proposal endorsement, prpBytes, err := plugin.Endorse(prpBytes, ctx.SignedProposal) if err != nil { endorserLogger.Warning("Endorsement with plugin for", ctx, " failed:", err) return nil, errors.WithStack(err) } resp := &pb.ProposalResponse{ Version: 1, Endorsement: endorsement, Payload: prpBytes, Response: ctx.Response, } endorserLogger.Debug("Exiting", ctx) return resp, nil }
The default endorsement plug-in provided by the system is as follows, which is essentially to sign the transaction execution result and the information of the Proposal signer.
// Endorse signs the given payload(ProposalResponsePayload bytes), and optionally mutates it. // Returns: // The Endorsement: A signature over the payload, and an identity that is used to verify the signature // The payload that was given as input (could be modified within this function) // Or error on failure func (e *DefaultEndorsement) Endorse(prpBytes []byte, sp *peer.SignedProposal) (*peer.Endorsement, []byte, error) { // Extract the signer of Proposal signer, err := e.SigningIdentityForRequest(sp) if err != nil { return nil, nil, errors.New(fmt.Sprintf("failed fetching signing identity: %v", err)) } // Get signer identity // serialize the signing identity identityBytes, err := signer.Serialize() if err != nil { return nil, nil, errors.New(fmt.Sprintf("could not serialize the signing identity: %v", err)) } // Sign Payload and identity // sign the concatenation of the proposal response and the serialized endorser identity with this endorser's key signature, err := signer.Sign(append(prpBytes, identityBytes...)) if err != nil { return nil, nil, errors.New(fmt.Sprintf("could not sign the proposal response payload: %v", err)) } endorsement := &peer.Endorsement{Signature: signature, Endorser: identityBytes} return endorsement, prpBytes, nil }
Send Response
ProcessProposal will take the ProposalResponse as the return value, and the rest will be handed over to gRPC and sent to the requester.
summary
From the macro and source level, this paper interprets the data structure involved in endorsement of transaction proposal and its main endorsement process. The core can mainly include the following steps:
- Check Proposal
- Create a simulator for the transaction and call the simulator to simulate the execution of the transaction and generate the execution result
- The endorsement module endorses (signs) the execution result and the Proposal identity information, and then generates an endorsement response to send to the client
As for endorsement process, the links not involved in this paper are:
- The meaning of each field in Proposal
- Simulation execution transaction is a chain code execution function, the process of interacting with Peer, and various resources of simulation execution
- Implementation of two kinds of plug-in ESCC
In the following chapters, we will make further analysis of the relevant source code implementation.