Fabric源碼分析之Peer節(jié)點背書提案流程

environment:
fabric v1.4.2

1. 概述

Endorser節(jié)點是peer節(jié)點所扮演的一種角色褂痰,在peer啟動時會創(chuàng)建Endorser背書服務器亩进,并注冊到本地gRPC服務器(7051端口)上對外提供服務,對請求的簽名提案消息執(zhí)行啟動鏈碼容器缩歪、模擬執(zhí)行鏈碼归薛、背書簽名等流程。所有客戶端提交到賬本的調(diào)用交易都需要背書節(jié)點背書匪蝙,當客戶端收集到足夠的背書信息之后抓狭,再將簽名提案消息聪建、模擬執(zhí)行的結(jié)果以及背書信息打包成交易信息發(fā)給orderer節(jié)點排序出塊
背書者Endorser在一個交易流中充當?shù)淖饔萌缦拢?/p>

  • 客戶端發(fā)送一個背書申請(SignedProposal)到Endorser送挑。
  • Endorser對申請進行背書屯远,發(fā)送一個申請應答(ProposalResponse)到客戶端。
  • 客戶端將申請應答中的背書組裝到一個交易請求(SignedTransaction)中颤绕。

2. 背書服務初始化

定位到peer/node/start.goserve函數(shù)幸海,這個是peer節(jié)點的啟動初始化函數(shù),下面為關鍵的背書節(jié)點啟動語句:

serverEndorser := endorser.NewEndorserServer(privDataDist, endorserSupport, pr, metricsProvider)
...
// start the peer server
auth := authHandler.ChainFilters(serverEndorser, authFilters...)
// Register the Endorser server
// 設置完之后注冊背書服務
pb.RegisterEndorserServer(peerServer.Server(), auth)

背書服務最重要的接口為,位置為protos\peer\peer.pb.go:

// EndorserServer is the server API for Endorser service.
type EndorserServer interface {
    ProcessProposal(context.Context, *SignedProposal) (*ProposalResponse, error)
}

ProcessProposal()服務接口主要功能為接收和處理簽名提案消息(SignedProposal)奥务、啟動鏈碼容器物独、執(zhí)行調(diào)用鏈碼以及進行簽名背書。
函數(shù)定義的位置為core/endorser/endorser.go

3. 背書服務

在ProcessProposal()服務中氯葬,主要存在以下流程:

  • 首先對提案進行預處理preProcess()
    • 這一步主要就是對提案中的內(nèi)容進行相關驗證操作挡篓。
    • 驗證Header信息
    • 驗證證書信息
    • 判斷調(diào)用的鏈碼類型與通道信息。
  • 然后對提案進行模擬SimulateProposal()
    • 獲取調(diào)用的鏈碼的具體功能與參數(shù)溢谤。
    • 判斷鏈碼類型瞻凤,用戶鏈碼需要檢查實例化策略憨攒,系統(tǒng)鏈碼只獲取版本信息世杀。
    • 創(chuàng)建Tx模擬器阀参,調(diào)用callChaincode()方法進行模擬。
    • 記錄模擬時間瞻坝,執(zhí)行鏈碼蛛壳,判斷是否調(diào)用的是lscc,功能為upgrade或者為deploy所刀。如果是的話進行鏈碼的Init衙荐。
    • 對模擬完成的賬本進行快照,返回模擬結(jié)果集浮创。
  • 最后進行背書操作endorseProposal()
    • 獲取進行背書操作的鏈碼
    • 獲取鏈碼事件與鏈碼版本信息
    • 獲取背書所需要的插件忧吟,獲取調(diào)用鏈碼的相關數(shù)據(jù)
    • 通過獲取的插件進行背書操作
    • 返回背書響應

提案背書主要入口函數(shù)為ProcessProposal,后續(xù)都是圍繞此函數(shù)分析,源碼如下:

// ProcessProposal process the Proposal
func (e *Endorser) ProcessProposal(ctx context.Context, signedProp *pb.SignedProposal) (*pb.ProposalResponse, error) {
    // start time for computing elapsed time metric for successfully endorsed proposals
    // 首先獲取Peer節(jié)點處理提案開始的時間
    startTime := time.Now()
    // Peer節(jié)點接收到的提案數(shù)+1
    e.Metrics.ProposalsReceived.Add(1)
    // 從上下文中獲取發(fā)起提案的地址
    addr := util.ExtractRemoteAddress(ctx)
    // 日志輸出
    endorserLogger.Debug("Entering: request from", addr)

    // variables to capture proposal duration metric
    // 這個不是鏈碼ID斩披,是通道ID
    var chainID string
    var hdrExt *pb.ChaincodeHeaderExtension
    var success bool
    // 這個會在方法結(jié)束的時候調(diào)用
    defer func() {
        // capture proposal duration metric. hdrExt == nil indicates early failure
        // where we don't capture latency metric. But the ProposalValidationFailed
        // counter metric should shed light on those failures.
        // 判斷chaincodeHeaderExtension是否為空溜族,如果為空的話提案驗證失敗
        if hdrExt != nil {
            meterLabels := []string{
                "channel", chainID,
                "chaincode", hdrExt.ChaincodeId.Name + ":" + hdrExt.ChaincodeId.Version,
                "success", strconv.FormatBool(success),
            }
            e.Metrics.ProposalDuration.With(meterLabels...).Observe(time.Since(startTime).Seconds())
        }

        endorserLogger.Debug("Exit: request from", addr)
    }()

    // 0 -- check and validate
    // 到了第一個重要的方法,對已簽名的提案進行預處理垦沉,點進行看一下
    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

    // 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
    // 這里定義了一個Tx模擬器煌抒,用于后面的模擬交易過程,如果通道Id為空,那么TxSimulator也是空
    var txsim ledger.TxSimulator
    // 定義一個歷史記錄查詢器
    var historyQueryExecutor ledger.HistoryQueryExecutor
    // 判斷是否需要Tx模擬
    if acquireTxSimulator(chainID, vr.hdrExt.ChaincodeId) {
        // 根據(jù)通道ID獲取Tx模擬器
        if txsim, err = e.s.GetTxSimulator(chainID, txid); err != nil {
            return &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}, nil
        }

        // txsim acquires a shared lock on the stateDB. As this would impact the block commits (i.e., commit
        // of valid write-sets to the stateDB), we must release the lock as early as possible.
        // Hence, this txsim object is closed in simulateProposal() as soon as the tx is simulated and
        // rwset is collected before gossip dissemination if required for privateData. For safety, we
        // add the following defer statement and is useful when an error occur. Note that calling
        // txsim.Done() more than once does not cause any issue. If the txsim is already
        // released, the following txsim.Done() simply returns.
        defer txsim.Done()
        // 獲取歷史記錄查詢器
        if historyQueryExecutor, err = e.s.GetHistoryQueryExecutor(chainID); err != nil {
            return &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}, nil
        }
    }
    // 定義一個交易參數(shù)結(jié)構體厕倍,用于下面的方法,里面的字段之前都有說過
    txParams := &ccprovider.TransactionParams{
        ChannelID:            chainID,
        TxID:                 txid,
        SignedProp:           signedProp,
        Proposal:             prop,
        TXSimulator:          txsim,
        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

    // 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
        }
    }

    // 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)

        // if error, capture endorsement failure metric
        meterLabels := []string{
            "channel", chainID,
            "chaincode", hdrExt.ChaincodeId.Name + ":" + hdrExt.ChaincodeId.Version,
        }

        if err != nil {
            meterLabels = append(meterLabels, "chaincodeerror", strconv.FormatBool(false))
            e.Metrics.EndorsementsFailed.With(meterLabels...).Add(1)
            return &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}, nil
        }
        if pResp.Response.Status >= shim.ERRORTHRESHOLD {
            // the default ESCC treats all status codes about threshold as errors and fails endorsement
            // useful to track this as a separate metric
            meterLabels = append(meterLabels, "chaincodeerror", strconv.FormatBool(true))
            e.Metrics.EndorsementsFailed.With(meterLabels...).Add(1)
            endorserLogger.Debugf("[%s][%s] endorseProposal() resulted in chaincode %s error for txid: %s", chainID, shorttxid(txid), hdrExt.ChaincodeId, txid)
            return pResp, nil
        }
    }

    // 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
}

3.1 檢查和校驗簽名提案的合法性

preProcess()方法對簽名提案消息進行預處理寡壮,主要包括驗證消息格式和簽名的合法性、驗證提案消息對應鏈碼檢查是否是系統(tǒng)鏈碼并且不為外部調(diào)用讹弯、交易的唯一性况既、驗證是否滿足對應通道的訪問控制策略。

// preProcess checks the tx proposal headers, uniqueness and ACL
func (e *Endorser) preProcess(signedProp *pb.SignedProposal) (*validateResult, error) {
    vr := &validateResult{}
    // at first, we check whether the message is valid
    // 驗證信息是否有效
    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
    }
    // 從提案的Header中獲取通道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
    }
    //獲取簽名域的Header
    shdr, err := putils.GetSignatureHeader(hdr.SignatureHeader)
    if err != nil {
        vr.resp = &pb.ProposalResponse{Response: &pb.Response{Status: 500, Message: err.Error()}}
        return vr, err
    }

    // block invocations to security-sensitive system chaincodes
    // 根據(jù)提案消息頭部hdrExt.ChaincodeId.Name鏈碼名檢查鏈碼是否為允許外部調(diào)用的系統(tǒng)鏈碼
    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,
        }

        // 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
        }

        // 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
    }

    vr.prop, vr.hdrExt, vr.chainID, vr.txid = prop, hdrExt, chainID, txid
    return vr, nil
}

3.1.1 驗證消息格式和簽名合法性

preProcess()調(diào)用ValidateProposalMessage()對消息進行驗證组民,主要針對消息的格式棒仍、簽名、交易id進行驗證邪乍。
core/common/validation/msgvalidation.go找到ValidateProposalMessage函數(shù)

// 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
    // 從提案中獲取Proposal內(nèi)容
    prop, err := utils.GetProposal(signedProp.ProposalBytes)
    if err != nil {
        return nil, nil, nil, err
    }

    // 1) look at the ProposalHeader
    // 從Proposal中獲取Header
    hdr, err := utils.GetHeader(prop.Header)
    if err != nil {
        return nil, nil, nil, err
    }

    // validate the header
    // 對header進行驗證
    chdr, shdr, err := validateCommonHeader(hdr)
    if err != nil {
        return nil, nil, nil, err
    }

    // 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)
    }

    // 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.
    // 對交易id進行驗證降狠,驗證交易id是否與計算的交易id一致
    err = utils.CheckTxID(
        chdr.TxId,
        shdr.Nonce,
        shdr.Creator)
    if err != nil {
        return nil, nil, nil, err
    }

    // continue the validation in a way that depends on the type specified in the header
    // 根據(jù)消息類型進行分類處理
    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:
        // 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))
    }
}

validateCommonHeader()校驗Proposal.Header的合法性

// checks for a valid Header
func validateCommonHeader(hdr *common.Header) (*common.ChannelHeader, *common.SignatureHeader, error) {
    if hdr == nil {
        return nil, nil, errors.New("nil header")
    }

    chdr, err := utils.UnmarshalChannelHeader(hdr.ChannelHeader)
    if err != nil {
        return nil, nil, err
    }

    shdr, err := utils.GetSignatureHeader(hdr.SignatureHeader)
    if err != nil {
        return nil, nil, err
    }
    // 校驗消息類型是否屬于HeaderType_ENDORSER_TRANSACTION、HeaderType_CONFIG_UPDATE庇楞、HeaderType_CONFIG榜配、HeaderType_TOKEN_TRANSACTION,并且校驗Epoch是否為0
    err = validateChannelHeader(chdr)
    if err != nil {
        return nil, nil, err
    }
    // 校驗shdr shdr.Nonce  shdr.Creator是否為nil吕晌,或長度是否為0
    err = validateSignatureHeader(shdr)
    if err != nil {
        return nil, nil, err
    }

    return chdr, shdr, nil
}

checkSignatureFromCreator()對簽名進行校驗

// given a creator, a message and a signature,
// this function returns nil if the creator
// is a valid cert and the signature is valid
func checkSignatureFromCreator(creatorBytes []byte, sig []byte, msg []byte, ChainID string) error {
    putilsLogger.Debugf("begin")

    // check for nil argument
    if creatorBytes == nil || sig == nil || msg == nil {
        return errors.New("nil arguments")
    }

    mspObj := mspmgmt.GetIdentityDeserializer(ChainID)
    if mspObj == nil {
        return errors.Errorf("could not get msp for channel [%s]", ChainID)
    }

    // get the identity of the creator
    creator, err := mspObj.DeserializeIdentity(creatorBytes)
    if err != nil {
        return errors.WithMessage(err, "MSP error")
    }

    putilsLogger.Debugf("creator is %s", creator.GetIdentifier())

    // ensure that creator is a valid certificate
    err = creator.Validate()
    if err != nil {
        return errors.WithMessage(err, "creator certificate is not valid")
    }

    putilsLogger.Debugf("creator is valid")

    // validate the signature
    err = creator.Verify(msg, sig)
    if err != nil {
        return errors.WithMessage(err, "creator's signature over the proposal is not valid")
    }

    putilsLogger.Debugf("exits successfully")

    return nil
}

3.1.2 檢查是否是系統(tǒng)鏈碼并且不為外部調(diào)用

定位到core/scc/sccproviderimpl.goIsSysCCAndNotInvokableExternal函數(shù)
簡單理解就是鏈碼是否可以為外部調(diào)用

// IsSysCCAndNotInvokableExternal returns true if the chaincode
// is a system chaincode and *CANNOT* be invoked through
// a proposal to this peer
func (p *Provider) IsSysCCAndNotInvokableExternal(name string) bool {
    for _, sysCC := range p.SysCCs {
        if sysCC.Name() == name {
            return !sysCC.InvokableExternal()
        }
    }

    if isDeprecatedSysCC(name) {
        return true
    }

    return false
}

func isDeprecatedSysCC(name string) bool {
    return name == "vscc" || name == "escc"
}

3.1.3 檢查簽名提案消息交易id的唯一性

首先查看是否存在該賬本蛋褥,然后查看賬本是否存在該交易id。

// GetTransactionByID retrieves a transaction by id
func (s *SupportImpl) GetTransactionByID(chid, txID string) (*pb.ProcessedTransaction, error) {
    lgr := s.Peer.GetLedger(chid)
    if lgr == nil {
        return nil, errors.Errorf("failed to look up the ledger for Channel %s", chid)
    }
    tx, err := lgr.GetTransactionByID(txID)
    if err != nil {
        return nil, errors.WithMessage(err, "GetTransactionByID failed")
    }
    return tx, nil
}

3.1.4 驗證是否滿足對應通道的訪問控制策略

背書節(jié)點在背書過程中會檢查是否滿足應用通道的Writers策略

// CheckACL checks the ACL for the resource for the Channel using the
// SignedProposal from which an id can be extracted for testing against a policy
func (s *SupportImpl) CheckACL(signedProp *pb.SignedProposal, chdr *common.ChannelHeader, shdr *common.SignatureHeader, hdrext *pb.ChaincodeHeaderExtension) error {
    return s.ACLProvider.CheckACL(resources.Peer_Propose, chdr.ChannelId, signedProp)
}

3.2 調(diào)用鏈碼并模擬執(zhí)行提案

首先睛驳,ProcessProposal()方法調(diào)用方法acquireTxSimulator()根據(jù)鏈碼判斷是否需要創(chuàng)建交易模擬器TxSimulator烙心,如果需要則創(chuàng)建交易模擬器TxSimulator(無法查詢歷史記錄)以及歷史記錄查詢器HistoryQueryExecutor膜廊,接著再調(diào)用SimulateProposal()模擬執(zhí)行交易提案消息,并返回模擬執(zhí)行結(jié)果淫茵。
其中爪瓜,鏈碼qscc、cscc不需要交易模擬器匙瘪。

// determine whether or not a transaction simulator should be
// obtained for a proposal.
func acquireTxSimulator(chainID string, ccid *pb.ChaincodeID) bool {
    // 如果通道ID為空,就說明不需要進行Tx的模擬
    if chainID == "" {
        return false
    }

    // ˉ\_(ツ)_/ˉ locking.
    // Don't get a simulator for the query and config system chaincode.
    // These don't need the simulator and its read lock results in deadlocks.
    // 通道ID不為空铆铆,則判斷鏈碼的類型,如果是qscc(查詢系統(tǒng)鏈碼),cscc(配置系統(tǒng)鏈碼)丹喻,則不需要進行Tx模擬
    switch ccid.Name {
    case "qscc", "cscc":
        return false
    default:
        return true
    }
}
// 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
    // 獲取鏈碼調(diào)用的細節(jié)
    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) { // 不是系統(tǒng)鏈碼
        // 獲取鏈碼的標準數(shù)據(jù)結(jié)構
        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 {
        // 如果調(diào)用的是系統(tǒng)鏈碼薄货,僅僅獲取系統(tǒng)鏈碼的版本
        version = util.GetSysCCVersion()
    }

    // ---3. execute the proposal and get simulation results
    var simResult *ledger.TxSimulationResults   // 定義一個Tx模擬結(jié)果集
    var pubSimResBytes []byte                   // 一個byte數(shù)組,保存public的模擬響應結(jié)果
    var res *pb.Response                        // 響應信息
    var ccevent *pb.ChaincodeEvent              // 鏈碼事件
    // 執(zhí)行鏈碼進行模擬
    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 {
        // GetTxSimulationResults()獲取Tx模擬結(jié)果集
        if simResult, err = txParams.TXSimulator.GetTxSimulationResults(); err != nil {
            txParams.TXSimulator.Done()
            return nil, nil, nil, nil, err
        }
        // 之前提到Tx模擬結(jié)果集中不僅僅只有公共讀寫集碍论,還有私有的讀寫集,接下來判斷私有的讀寫集是否為空
        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")
            }
            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
            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
}

3.2.1 檢查實例化策略

core/common/ccprovider/ccprovider.go/CheckInstantiationPolicy()會調(diào)用GetChaincodeData()嘗試從緩存或者本地文件系統(tǒng)獲取已安裝的鏈碼包CCPackage谅猾,再解析成ChaincodeData對象ccdata。再與賬本中保存的對應鏈碼的實例化策略進行比較鳍悠。

func CheckInstantiationPolicy(name, version string, cdLedger *ChaincodeData) error {
    ccdata, err := GetChaincodeData(name, version)
    if err != nil {
        return err
    }

    // we have the info from the fs, check that the policy
    // matches the one on the file system if one was specified;
    // this check is required because the admin of this peer
    // might have specified instantiation policies for their
    // chaincode, for example to make sure that the chaincode
    // is only instantiated on certain channels; a malicious
    // peer on the other hand might have created a deploy
    // transaction that attempts to bypass the instantiation
    // policy. This check is there to ensure that this will not
    // happen, i.e. that the peer will refuse to invoke the
    // chaincode under these conditions. More info on
    // https://jira.hyperledger.org/browse/FAB-3156
    if ccdata.InstantiationPolicy != nil {
        if !bytes.Equal(ccdata.InstantiationPolicy, cdLedger.InstantiationPolicy) {
            return fmt.Errorf("Instantiation policy mismatch for cc %s/%s", name, version)
        }
    }

    return nil
}

3.2.2 調(diào)用鏈碼

SimulateProposal()方法中税娜,會調(diào)用callChaincode()方法調(diào)用鏈碼。

// 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) {
    endorserLogger.Infof("[%s][%s] Entry chaincode: %s", txParams.ChannelID, shorttxid(txParams.TxID), cid)
    defer func(start time.Time) {
        logger := endorserLogger.WithOptions(zap.AddCallerSkip(1))
        elapsedMilliseconds := time.Since(start).Round(time.Millisecond) / time.Millisecond
        logger.Infof("[%s][%s] Exit chaincode: %s (%dms)", txParams.ChannelID, shorttxid(txParams.TxID), cid, elapsedMilliseconds)
    }(time.Now())

    var err error
    var res *pb.Response
    var ccevent *pb.ChaincodeEvent

    // is this a system chaincode
    // 執(zhí)行鏈碼贼涩,如果是用戶鏈碼具體怎么執(zhí)行的要看用戶寫的鏈碼邏輯巧涧,執(zhí)行完畢后返回響應信息與鏈碼事件
    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
    }

    // per doc anything < 400 can be sent as TX.
    // fabric errors will always be >= 400 (ie, unambiguous errors )
    // "lscc" will respond with status 200 or 500 (ie, unambiguous OK or ERROR)
    // 狀態(tài)常量一共有三個:OK = 200 ERRORTHRESHOLD = 400 ERROR = 500 大于等于400就是錯誤信息或者被背書節(jié)點拒絕。
    if res.Status >= shim.ERRORTHRESHOLD {
        return res, nil, nil
    }

    // ----- BEGIN -  SECTION THAT MAY NEED TO BE DONE IN LSCC ------
    // if this a call to deploy a chaincode, We need a mechanism
    // to pass TxSimulator into LSCC. Till that is worked out this
    // special code does the actual deploy, upgrade here so as to collect
    // all state under one TxSimulator
    //
    // NOTE that if there's an error all simulation, including the chaincode
    // table changes in lscc will be thrown away
    // 判斷調(diào)用的鏈碼是否為lscc,如果是lscc判斷傳入的參數(shù)是否大于等于3遥倦,并且調(diào)用的方法是否為deploy或者upgrade谤绳,如果是用戶鏈碼到這是方法就結(jié)束了。
    // 用戶鏈碼的實例化(deploy)和升級(upgrade)就會進來這里
    if cid.Name == "lscc" && len(input.Args) >= 3 && (string(input.Args[0]) == "deploy" || string(input.Args[0]) == "upgrade") {
        // 獲取鏈碼部署的基本結(jié)構,deploy與upgrade都需要對鏈碼進行部署
        userCDS, err := putils.GetChaincodeDeploymentSpec(input.Args[2], e.PlatformRegistry)
        if err != nil {
            return nil, nil, err
        }

        var cds *pb.ChaincodeDeploymentSpec
        cds, err = e.SanitizeUserCDS(userCDS)
        if err != nil {
            return nil, nil, err
        }

        // this should not be a system chaincode
        if e.s.IsSysCC(cds.ChaincodeSpec.ChaincodeId.Name) {
            return nil, nil, errors.Errorf("attempting to deploy a system chaincode %s/%s", cds.ChaincodeSpec.ChaincodeId.Name, txParams.ChannelID)
        }
        // 執(zhí)行鏈碼的Init,具體如何執(zhí)行的這里就不再看了,不然內(nèi)容更多了
        _, _, err = e.s.ExecuteLegacyInit(txParams, txParams.ChannelID, cds.ChaincodeSpec.ChaincodeId.Name, cds.ChaincodeSpec.ChaincodeId.Version, txParams.TxID, txParams.SignedProp, txParams.Proposal, cds)
        if err != nil {
            // increment the failure to indicate instantion/upgrade failures
            meterLabels := []string{
                "channel", txParams.ChannelID,
                "chaincode", cds.ChaincodeSpec.ChaincodeId.Name + ":" + cds.ChaincodeSpec.ChaincodeId.Version,
            }
            e.Metrics.InitFailed.With(meterLabels...).Add(1)
            return nil, nil, err
        }
    }
    // ----- END -------

    return res, ccevent, err
}

執(zhí)行Execute()方法調(diào)用鏈碼袒哥,然后在針對deployupgrade操作進行處理缩筛。
首先看看Execute()位于core/chaincode/chaincode_support.go

// Execute invokes chaincode and returns the original response.
func (cs *ChaincodeSupport) Execute(txParams *ccprovider.TransactionParams, cccid *ccprovider.CCContext, input *pb.ChaincodeInput) (*pb.Response, *pb.ChaincodeEvent, error) {
    // 主要是啟動鏈碼容器,調(diào)用鏈碼
    resp, err := cs.Invoke(txParams, cccid, input)
    // 對鏈碼執(zhí)行結(jié)果進行處理
    return processChaincodeExecutionResult(txParams.TxID, cccid.Name, resp, err)
}

繼續(xù)看看Invoke主要調(diào)用了Launch啟動鏈碼容器堡称,和execute給鏈碼容器grpc消息(ChaincodeMessage_TRANSACTION)進行通信瞎抛,有興趣的童鞋們可以跟蹤下去

// Invoke will invoke chaincode and return the message containing the response.
// The chaincode will be launched if it is not already running.
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
    }

    // TODO add Init exactly once semantics here once new lifecycle
    // is available.  Enforced if the target channel is using the new lifecycle
    //
    // First, the function name of the chaincode to invoke should be checked.  If it is
    // "init", then consider this invocation to be of type pb.ChaincodeMessage_INIT,
    // otherwise consider it to be of type pb.ChaincodeMessage_TRANSACTION,
    //
    // Secondly, A check should be made whether the chaincode has been
    // inited, then, if true, only allow cctyp pb.ChaincodeMessage_TRANSACTION,
    // otherwise, only allow cctype pb.ChaincodeMessage_INIT,
    cctype := pb.ChaincodeMessage_TRANSACTION
    // 給鏈碼發(fā)送消息
    return cs.execute(cctype, txParams, cccid, input, h)
}

3.2.3 處理模擬執(zhí)行結(jié)果

執(zhí)行完鏈碼后結(jié)果不會馬上寫到數(shù)據(jù)庫,而是以讀寫集的形式返回給客戶端却紧,結(jié)果寫入交易模擬器TXSimulator中桐臊。通過調(diào)用GetTxSimulationResults()方法可以獲取模擬執(zhí)行結(jié)果。TxSimulationResults包含公有數(shù)據(jù)讀寫集PubSimulationResults以及私有數(shù)據(jù)讀寫集PvtSimulationResults晓殊。
SimulateProposal()方法會調(diào)用GetTxSimulationResults()方法獲取模擬執(zhí)行結(jié)果断凶。那先看看此函數(shù),位于core/ledger/kvledger/txmgmt/rwsetutil/rwset_builder.go

// GetTxSimulationResults returns the proto bytes of public rwset
// (public data + hashes of private data) and the private rwset for the transaction
func (b *RWSetBuilder) GetTxSimulationResults() (*ledger.TxSimulationResults, error) {
    // 獲取交易模擬執(zhí)行結(jié)果的交易私密數(shù)據(jù)讀寫集
    pvtData := b.getTxPvtReadWriteSet()
    var err error

    var pubDataProto *rwset.TxReadWriteSet
    var pvtDataProto *rwset.TxPvtReadWriteSet

    // Populate the collection-level hashes into pub rwset and compute the proto bytes for pvt rwset
    // 計算私密數(shù)據(jù)hash
    if pvtData != nil {
        if pvtDataProto, err = pvtData.toProtoMsg(); err != nil {
            return nil, err
        }
        // 遍歷計算私密數(shù)據(jù)hash值
        for _, ns := range pvtDataProto.NsPvtRwset {
            for _, coll := range ns.CollectionPvtRwset {
                b.setPvtCollectionHash(ns.Namespace, coll.CollectionName, coll.Rwset)
            }
        }
    }
    // Compute the proto bytes for pub rwset
    // 獲取交易模擬執(zhí)行結(jié)果的公有數(shù)據(jù)讀寫集
    pubSet := b.GetTxReadWriteSet()
    if pubSet != nil {
        if pubDataProto, err = b.GetTxReadWriteSet().toProtoMsg(); err != nil {
            return nil, err
        }
    }
    // 構造交易模擬執(zhí)行結(jié)果
    return &ledger.TxSimulationResults{
        PubSimulationResults: pubDataProto,
        PvtSimulationResults: pvtDataProto,
    }, nil
}

3.3 簽名背書

ProcessProposal()方法中巫俺,首先會判斷通道id是否為nil认烁,如果為nil,則直接返回響應結(jié)果(例如install操作)。如果不為nil却嗡,會調(diào)用endorseProposal()方法對模擬執(zhí)行結(jié)果進行簽名和背書舶沛。在endorseProposal()方法中,會構造Context對象窗价,再調(diào)用EndorseWithPlugin()里面會調(diào)用getOrCreatePlugin()創(chuàng)建plugin如庭,然后調(diào)用proposalResponsePayloadFromContext()方法,在該方法中會計算背書結(jié)果hash以及封裝模擬執(zhí)行結(jié)果舌镶、鏈碼event事件以及鏈碼響應結(jié)果等(數(shù)據(jù)結(jié)構為ProposalResponsePayload)柱彻,在序列化成[]byte數(shù)組豪娜,最后調(diào)用Endorse()方法執(zhí)行簽名背書操作(由于escc現(xiàn)在是插件形式執(zhí)行餐胀,里面會進行判斷。默認執(zhí)行escc

// 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))

    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
    // 判斷是否是系統(tǒng)鏈碼
    if isSysCC { // 如果是系統(tǒng)鏈碼瘤载,則使用escc進行背書
        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 ""
        // 獲取系統(tǒng)鏈碼版本
        ccid.Version = util.GetSysCCVersion()
    } else {
        // 獲取用戶鏈碼版本
        ccid.Version = cd.CCVersion()
    }

    ctx := Context{
        PluginName:     escc,
        Channel:        chainID,
        SignedProposal: signedProp,
        ChaincodeID:    ccid,
        Event:          eventBytes,
        SimRes:         simRes,
        Response:       response,
        Visibility:     visibility,
        Proposal:       proposal,
        TxID:           txid,
    }
    // 背書
    return e.s.EndorseWithPlugin(ctx)
}

接著看EndorseWithPlugin,位于core/endorser/plugin_endorser.go

// 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
    }
    // 獲取或者創(chuàng)建插件
    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)
    }
    // 從上下文中獲取提案byte數(shù)據(jù)
    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")
    }
    // 進行背書操作
    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)
    }
    // 背書完成后否灾,封裝為提案響應結(jié)構體,最后將該結(jié)構體返回
    resp := &pb.ProposalResponse{
        Version:     1,
        Endorsement: endorsement,
        Payload:     prpBytes,
        Response:    ctx.Response,
    }
    endorserLogger.Debug("Exiting", ctx)
    return resp, nil
}

背書操作主要是在Endorse進行鸣奔,位于core/handlers/endorsement/plugin/plugin.go

// 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) {
    signer, err := e.SigningIdentityForRequest(sp)
    if err != nil {
        return nil, nil, errors.New(fmt.Sprintf("failed fetching signing identity: %v", err))
    }
    // 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 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
}

github

參考:
Fabric1.4源碼解析:Peer節(jié)點背書提案過程
Fabric 1.4 源碼分析 Endorser背書節(jié)點

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末墨技,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子挎狸,更是在濱河造成了極大的恐慌扣汪,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锨匆,死亡現(xiàn)場離奇詭異崭别,居然都是意外死亡,警方通過查閱死者的電腦和手機恐锣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門茅主,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人土榴,你說我怎么就攤上這事诀姚。” “怎么了玷禽?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵赫段,是天一觀的道長。 經(jīng)常有香客問我矢赁,道長糯笙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任坯台,我火速辦了婚禮炬丸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己稠炬,他們只是感情好焕阿,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著首启,像睡著了一般暮屡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上毅桃,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天褒纲,我揣著相機與錄音,去河邊找鬼钥飞。 笑死莺掠,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的读宙。 我是一名探鬼主播彻秆,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼结闸!你這毒婦竟也來了唇兑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤桦锄,失蹤者是張志新(化名)和其女友劉穎扎附,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體结耀,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡留夜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了饼记。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片香伴。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖具则,靈堂內(nèi)的尸體忽然破棺而出即纲,到底是詐尸還是另有隱情,我是刑警寧澤博肋,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布低斋,位于F島的核電站,受9級特大地震影響匪凡,放射性物質(zhì)發(fā)生泄漏膊畴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一病游、第九天 我趴在偏房一處隱蔽的房頂上張望唇跨。 院中可真熱鬧稠通,春花似錦、人聲如沸买猖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玉控。三九已至飞主,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間高诺,已是汗流浹背碌识。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留虱而,地道東北人筏餐。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像薛窥,于是被迫代替她去往敵國和親胖烛。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355