聊聊長(zhǎng)安鏈的技術(shù)設(shè)計(jì)(二)

聊聊長(zhǎng)安鏈的技術(shù)設(shè)計(jì)(二)

繼續(xù)。

長(zhǎng)安鏈:不好的地方

終于到了不好的地方了,對(duì)下面的每一點(diǎn),我盡量做到“理由充分”,至少能說(shuō)服我自己吧。

Gas 的使用

長(zhǎng)安鏈的智能合約運(yùn)行支持Gas,代碼中出現(xiàn) Gas 的地方很多,比如,

//  -- tx_sim_context.go
    func (s *txSimContextImpl) CallContract(contractId *commonpb.ContractId, method string, byteCode []byte, 
parameter map[string]string, gasUsed uint64, refTxType commonpb.TxType) (*commonpb.ContractResult, commonpb.TxStatusCode) {
......
    r, code := s.vmManager.RunContract(contractId, method, byteCode, parameter, s, s.gasUsed, refTxType)


//  -- instance.go
    func CreateInstance(contextId int64, code exec.Code, method string, contractId *commonPb.ContractId, 
gasUsed uint64, gasLimit int64) (*wxvmInstance, error) {

不得不說(shuō),這是一個(gè)不必要的設(shè)計(jì)。

Gas 最早應(yīng)該是出現(xiàn)在以太坊項(xiàng)目中,彼時(shí)應(yīng)該還沒(méi)有聯(lián)盟鏈的概念,所有的區(qū)塊鏈都是公有鏈。以太坊提出了智能合約概念,這個(gè)在當(dāng)時(shí)是先進(jìn)的,但是并沒(méi)有機(jī)制保證所有的智能合約都是“善意的”。例如,如果有一個(gè)智能合約,故意寫(xiě)了一個(gè)死循環(huán),那么執(zhí)行這個(gè)合約的節(jié)點(diǎn)就直接服務(wù)宕機(jī)了。為了防止這樣的情況出現(xiàn),以太坊引入了 Gas 機(jī)制,智能合約中每執(zhí)行一次操作,都要消耗一定量的 Gas,執(zhí)行智能合約的時(shí)候還需要傳入?yún)?shù) GasLimit,表示此次執(zhí)行合約所需的 Gas 的上限,如果當(dāng)使用的 Gas 超出了這個(gè)上限,就停止合約執(zhí)行,將此交易標(biāo)記為無(wú)效。

可見(jiàn),這個(gè)機(jī)制是應(yīng)對(duì)公有鏈網(wǎng)絡(luò)中的不確定性,而想出來(lái)的辦法。那在聯(lián)盟鏈產(chǎn)品中,這樣的特性還是必要的嗎?我認(rèn)為完全不必要。首先,聯(lián)盟鏈中的節(jié)點(diǎn)相對(duì)較少,網(wǎng)絡(luò)較為封閉,不會(huì)對(duì)公網(wǎng)開(kāi)放訪問(wèn)(否則就變成公有鏈了),在節(jié)點(diǎn)受限的情況下,智能合約中有害代碼的概率本身就大幅度降低,雖然不可能降低到0。其次,在節(jié)點(diǎn)受限的場(chǎng)景下,有很多其他的方案可以作為替代進(jìn)行合約代碼控制,甚至可以加入人為干預(yù)的流程,例如,人工智能合約代碼審查(不要笑,這個(gè)方案現(xiàn)實(shí)中很管用);復(fù)雜一些的,可以參考 Fabric 的 Endorse 機(jī)制和 Chaincode 生命周期管理機(jī)制。除此之外,智能合約的引擎中不需要處理 Gas 相關(guān)的邏輯,對(duì)系統(tǒng)也是一種簡(jiǎn)化。

我相信,長(zhǎng)安鏈的設(shè)計(jì)者應(yīng)該也是認(rèn)為 Gas 是一個(gè)不必要的設(shè)計(jì),因?yàn)樵诖a中,所有的 GasLimit 都設(shè)置為了一個(gè)很大的常數(shù)值,

//  -- vm_interface.go
const (
    GasLimit            = 1e10    // invoke user contract max gas
    TimeLimit           = 1 * 1e9 // 1s

說(shuō)明,長(zhǎng)安鏈的設(shè)計(jì)者也認(rèn)為,不需要使用 Gas 機(jī)制來(lái)進(jìn)行控制。Gas 目前還存在在代碼里面的原因可能是,智能合約的虛擬機(jī)(VM)代碼里面本身需要使用 Gas,而VM的代碼可能是從已有的公有鏈代碼移植過(guò)來(lái)的,為了適配舊的VM代碼 Gas 機(jī)制被留存了。也有另一種可能,將來(lái)長(zhǎng)安鏈?zhǔn)遣皇菚?huì)有公有鏈化的可能?這樣 Gas 機(jī)制就又能用了。

總之,站在聯(lián)盟鏈的角度上,Gas 機(jī)制無(wú)疑是一個(gè)壞設(shè)計(jì),這是項(xiàng)目中的一個(gè)技術(shù)債,有可能將來(lái)會(huì)被解決。項(xiàng)目 v1.0.0 版本就有技術(shù)債,感覺(jué)不太好。

校驗(yàn)身份證書(shū)的時(shí)機(jī)

作為聯(lián)盟鏈的一個(gè)重要特性——“準(zhǔn)入”,長(zhǎng)安鏈在這一點(diǎn)上做的還有很多不足。因?yàn)槁?lián)盟鏈的非公有屬性,導(dǎo)致其必須提供拒絕非法節(jié)點(diǎn)接入的特性,而長(zhǎng)安鏈很多地方忽略了這一點(diǎn)。

比如,區(qū)塊同步的請(qǐng)求的時(shí)候,最初是在這里注冊(cè)(register)同步請(qǐng)求處理方法,代碼如下,

//  -- blockchain_sync_server.go
    if err := sync.net.ReceiveMsg(netPb.NetMsg_SYNC_BLOCK_MSG, sync.blockSyncMsgHandler); err != nil {
        return err
    }

sync.net.ReceiveMsg 方法實(shí)際是完成一個(gè)注冊(cè)功能,當(dāng)收到 NetMsg_SYNC_BLOCK_MSG 請(qǐng)求的時(shí)候,調(diào)用 sync.blockSyncMsgHandler 來(lái)處理,而 sync.blockSyncMsgHandler 的代碼中,

//  -- blockchain_sync_server.go
func (sync *BlockChainSyncServer) blockSyncMsgHandler(from string, msg []byte, msgType netPb.NetMsg_MsgType) error {
    if atomic.LoadInt32(&sync.start) != 1 {
        return commonErrors.ErrSyncServiceHasStoped
    }
    if msgType != netPb.NetMsg_SYNC_BLOCK_MSG {
        return nil
    }
    var (
        err     error
        syncMsg = syncPb.SyncMsg{}
    )
    if err = proto.Unmarshal(msg, &syncMsg); err != nil {
        sync.log.Errorf("fail to proto.Unmarshal the syncPb.SyncMsg:%s", err.Error())
        return err
    }
    sync.log.Debugf("receive the NetMsg_SYNC_BLOCK_MSG:the Type is %d", syncMsg.Type)

    switch syncMsg.Type {
    case syncPb.SyncMsg_NODE_STATUS_REQ:
        return sync.handleNodeStatusReq(from)
......

進(jìn)入函數(shù)之后就進(jìn)行一些斷言判定,然后反序列化,最后就去執(zhí)行邏輯功能了。在該接口注冊(cè)的時(shí)候,sync.net.ReceiveMsg 也沒(méi)有包裝一層校驗(yàn)邏輯,來(lái)判斷請(qǐng)求者的身份是否有“準(zhǔn)入”的資格。換言之,長(zhǎng)安鏈在區(qū)塊同步的時(shí)候,沒(méi)有設(shè)置節(jié)點(diǎn)接入的門(mén)檻,節(jié)點(diǎn)上的數(shù)據(jù)可以被一個(gè)模擬的節(jié)點(diǎn),全部同步到鏈之外的地方。這根本是公有鏈的性質(zhì),而非聯(lián)盟鏈。

即使退一步來(lái)說(shuō),將長(zhǎng)安鏈定位為公有鏈,那么其共識(shí)機(jī)制只提供了raft、bft類(lèi)的機(jī)制,也是無(wú)法滿足公有鏈的要求。所以,目前長(zhǎng)安鏈實(shí)際上處于聯(lián)盟鏈和公有鏈之間的狀態(tài),無(wú)論作為公有鏈和聯(lián)盟鏈來(lái)看,都有較多的不足。

這個(gè)問(wèn)題不止在區(qū)塊同步的時(shí)候有,其他請(qǐng)求也有。當(dāng)然,這個(gè)問(wèn)題也不是那么難解決,在接入的地方加入身份證書(shū)驗(yàn)證即可,這本來(lái)就應(yīng)該是長(zhǎng)安鏈已經(jīng)提供的 Policy 機(jī)制的一部分。

簽名個(gè)數(shù)的問(wèn)題

下面幾個(gè)問(wèn)題都是和 Policy 機(jī)制相關(guān)的,先來(lái)看看第一個(gè)問(wèn)題,交易的簽名到底是一個(gè)列表還是單一的對(duì)象。先看代碼,交易類(lèi)型的定義是這樣,

//  -- transaction.pb.go
// a transaction includes request and its result
type Transaction struct {
    // header of the transaction
    Header *TxHeader `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
    // payload of the request
    RequestPayload []byte `protobuf:"bytes,2,opt,name=request_payload,json=requestPayload,proto3" json:"request_payload,omitempty"`
    // signature of request bytes(including header and payload)
    RequestSignature []byte `protobuf:"bytes,3,opt,name=request_signature,json=requestSignature,proto3" json:"request_signature,omitempty"`
    // result of the transaction, can be marshalled according to tx_type in header
    Result *Result `protobuf:"bytes,4,opt,name=result,proto3" json:"result,omitempty"`
}

protobuf 生成的代碼中看不出來(lái)什么,只知道簽名 RequestSignature 是一個(gè) byte 數(shù)組。再來(lái)看使用這個(gè)簽名的地方,

//  -- transaction.go
// verify transaction sender's authentication (include signature verification, cert-chain verification, access verification)
func verifyTxAuth(t *commonPb.Transaction, ac protocol.AccessControlProvider) error {
    var err error
    txBytes, err := CalcUnsignedTxBytes(t)
    if err != nil {
        return err
    }
    endorsements := []*commonPb.EndorsementEntry{{
        Signer:    t.Header.Sender,
        Signature: t.RequestSignature,
    }}
    resourceId, err := ac.LookUpResourceNameByTxType(t.Header.TxType)
    if err != nil {
        return err
    }
    principal, err := ac.CreatePrincipal(resourceId, endorsements, txBytes)
    if err != nil {
        return fmt.Errorf("fail to construct authentication principal: %s", err)
    }
    ok, err := ac.VerifyPrincipal(principal)
    if err != nil {
        return fmt.Errorf("authentication error, %s", err)
    }
    if !ok {
        return fmt.Errorf("authentication failed")
    }
    return nil
}

在節(jié)點(diǎn)驗(yàn)證簽名的時(shí)候,簽名驗(yàn)證的主入口函數(shù)是 verifyTxAuth,這里做了一個(gè)非常奇怪的轉(zhuǎn)換,把簽名 RequestSignature 轉(zhuǎn)換為只有一個(gè)元素的 EndorsementEntry 列表,然后再進(jìn)行構(gòu)造身份,身份驗(yàn)證(身份驗(yàn)證用的是之前提到的 Policy 機(jī)制)等邏輯處理。

這里我很謹(jǐn)慎的做一個(gè)判斷:長(zhǎng)安鏈的交易簽名只有1個(gè),之前提到的 Policy 機(jī)制在這種情況下,幾乎無(wú)法使用,可能只有 SELF、ANY 能勉強(qiáng)用一下。我做出這個(gè)判斷的時(shí)候我自己也嚇了一跳,畢竟長(zhǎng)安鏈引入 Policy 的機(jī)制其實(shí)也挺麻煩的,但引入之后卻沒(méi)有去用這個(gè)機(jī)制,這于情于理都無(wú)法解釋。但在我仔細(xì)查找了代碼之后,我還是做出了這個(gè)判斷。

實(shí)際上,Policy 機(jī)制的實(shí)現(xiàn)代碼還是很完善的,對(duì)不同的 ALL、MAJORITY、ANY、閾值、分?jǐn)?shù)等規(guī)則都有處理,但是調(diào)用的地方只有一個(gè)簽名。這說(shuō)明,長(zhǎng)安鏈在規(guī)劃中,是有計(jì)劃將 Policy 機(jī)制應(yīng)用好的,但是在客戶端提交交易前構(gòu)建簽名列表的時(shí)候,暫時(shí)還沒(méi)有加入多簽名的機(jī)制。這直接導(dǎo)致了 Policy 機(jī)制的殘缺,因此只能將這個(gè)問(wèn)題歸類(lèi)到壞設(shè)計(jì)里面。

交易到底由誰(shuí)來(lái)簽名、對(duì)什么簽名

還是簽名的問(wèn)題,再看一下交易的定義,

//  -- transaction.pb.go
// a transaction includes request and its result
type Transaction struct {
    // header of the transaction
    Header *TxHeader `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
    // payload of the request
    RequestPayload []byte `protobuf:"bytes,2,opt,name=request_payload,json=requestPayload,proto3" json:"request_payload,omitempty"`
    // signature of request bytes(including header and payload)
    RequestSignature []byte `protobuf:"bytes,3,opt,name=request_signature,json=requestSignature,proto3" json:"request_signature,omitempty"`
    // result of the transaction, can be marshalled according to tx_type in header
    Result *Result `protobuf:"bytes,4,opt,name=result,proto3" json:"result,omitempty"`
}

注釋說(shuō)的很清楚,簽名 RequestSignature 這個(gè)字段存的是對(duì)請(qǐng)求的簽名(包括 HeaderRequestPayload)。為什么是對(duì)請(qǐng)求的簽名,而不是對(duì)交易結(jié)果的簽名?

Fabric 中的 Policy 機(jī)制是對(duì)交易結(jié)果(而非交易請(qǐng)求)進(jìn)行簽名,在 Fabric 中,交易結(jié)果的主要數(shù)據(jù)結(jié)構(gòu)是讀寫(xiě)集(RwSet),這個(gè)結(jié)果是由不同節(jié)點(diǎn)的智能合約執(zhí)行得到的共同結(jié)果,節(jié)點(diǎn)通過(guò)對(duì)結(jié)果簽名,表示對(duì)此結(jié)果的背書(shū)(Endorsement)。因此,同一條交易才會(huì)有多個(gè)簽名,也因此,才會(huì)需要有背書(shū)的 Policy 機(jī)制來(lái)進(jìn)行驗(yàn)證。對(duì)比一下,

  1. 長(zhǎng)安鏈對(duì)交易請(qǐng)求數(shù)據(jù)進(jìn)行簽名;Fabric 對(duì)交易結(jié)果數(shù)據(jù)進(jìn)行簽名;
  2. 長(zhǎng)安鏈由交易發(fā)起方來(lái)簽名;Fabric 由交易執(zhí)行方來(lái)簽名;
  3. 長(zhǎng)安鏈的簽名只有1個(gè);Fabric 的簽名可以多個(gè);

正因?yàn)殚L(zhǎng)安鏈設(shè)計(jì)成對(duì)交易請(qǐng)求進(jìn)行簽名,所以只能由請(qǐng)求方來(lái)簽名;正是因?yàn)橛烧?qǐng)求方來(lái)簽名,而請(qǐng)求方通常只有一方,所以才導(dǎo)致了簽名只有1個(gè)。通常業(yè)務(wù)場(chǎng)景中,請(qǐng)求方多數(shù)是一方的時(shí)候居多。例如,寫(xiě)入訂單的場(chǎng)景,發(fā)起者就是下單的人,這個(gè)操作的請(qǐng)求方只有1個(gè);也有一些需要兩方請(qǐng)求的場(chǎng)景,如轉(zhuǎn)賬(即使是這個(gè)場(chǎng)景也是一方請(qǐng)求,很少雙方共同請(qǐng)求);需要三方或以上請(qǐng)求的業(yè)務(wù)場(chǎng)景就非常罕見(jiàn)了。

這樣分析的話,似乎就找到了交易中只有一個(gè)簽名的原因:長(zhǎng)安鏈在設(shè)計(jì) Policy 機(jī)制的時(shí)候,選擇了對(duì)交易請(qǐng)求進(jìn)行簽名。也因此導(dǎo)致了其 Policy 機(jī)制殘缺的現(xiàn)狀。

當(dāng)然,我并不是說(shuō) Policy 機(jī)制只有像 Fabric 中這樣用才是對(duì)的,其他用法只要邏輯自洽自然也完全可以,但目前長(zhǎng)安鏈的用法實(shí)在很難自圓其說(shuō)。

綜上,長(zhǎng)安鏈應(yīng)該只是把 Fabric 的 Policy 機(jī)制硬套在了其技術(shù)架構(gòu)上面,簽名既簽錯(cuò)了數(shù)據(jù),也由錯(cuò)誤的成員來(lái)簽名,導(dǎo)致了 Policy 機(jī)制在長(zhǎng)安鏈里幾乎沒(méi)發(fā)揮什么作用。

交易模型的問(wèn)題

這個(gè)問(wèn)題要說(shuō)清楚會(huì)比較長(zhǎng),單寫(xiě)一篇文章來(lái)說(shuō)吧,這里先跳過(guò)。

交易簽名沒(méi)有nonce

最后說(shuō)一個(gè)密碼相關(guān)的問(wèn)題吧,還是交易簽名。先對(duì)比下 Fabric 有關(guān)簽名的代碼,

//  -- transaction.pb.go
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"`

//  -- txutils.go
    paylBytes := MarshalOrPanic(
        &common.Payload{
            Header: MakePayloadHeader(payloadChannelHeader, payloadSignatureHeader),
            Data:   data,
        },
    )

    var sig []byte
    if signer != nil {
        sig, err = signer.Sign(paylBytes)
        if err != nil {
            return nil, err
        }
    }

最后的 sig, err = signer.Sign(paylBytes) 這一行會(huì)計(jì)算出簽名 sig,簽名的數(shù)據(jù) paylBytes 包括3個(gè)部分,ChannelHeaderSignatureHeaderDataData是數(shù)據(jù),無(wú)需多說(shuō);ChannelHeader包括一些鏈的基本信息,鏈名、消息版本、時(shí)間戳等等,非重點(diǎn);SignatureHeader包含2個(gè)信息,證書(shū)和nonce。

再看一下長(zhǎng)安鏈的實(shí)現(xiàn)代碼,這里需要再次請(qǐng)出交易結(jié)構(gòu)的代碼,

//  -- transaction.pb.go
// a transaction includes request and its result
type Transaction struct {
    // header of the transaction
    Header *TxHeader `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
    // payload of the request
    RequestPayload []byte `protobuf:"bytes,2,opt,name=request_payload,json=requestPayload,proto3" json:"request_payload,omitempty"`
    // signature of request bytes(including header and payload)
    RequestSignature []byte `protobuf:"bytes,3,opt,name=request_signature,json=requestSignature,proto3" json:"request_signature,omitempty"`
    // result of the transaction, can be marshalled according to tx_type in header
    Result *Result `protobuf:"bytes,4,opt,name=result,proto3" json:"result,omitempty"`
}

//  -- request.pb.go
// header of the request
type TxHeader struct {
    // blockchain identifier
    ChainId string `protobuf:"bytes,1,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"`
    // sender identifier
    Sender *accesscontrol.SerializedMember `protobuf:"bytes,2,opt,name=sender,proto3" json:"sender,omitempty"`
    // transaction type
    TxType TxType `protobuf:"varint,3,opt,name=tx_type,json=txType,proto3,enum=common.TxType" json:"tx_type,omitempty"`
    // transaction id set by sender, should be unique
    TxId string `protobuf:"bytes,4,opt,name=tx_id,json=txId,proto3" json:"tx_id,omitempty"`
    // transaction timestamp, in unix timestamp format, seconds
    Timestamp int64 `protobuf:"varint,5,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
    // expiration timestamp in unix timestamp format
    // after that the transaction is invalid if it is not included in block yet
    ExpirationTime int64 `protobuf:"varint,6,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"`
}

//  -- member.pb.go
// Serialized member of blockchain
type SerializedMember struct {
    // organization identifier of the member
    OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"`
    // member identity related info bytes
    MemberInfo []byte `protobuf:"bytes,2,opt,name=member_info,json=memberInfo,proto3" json:"member_info,omitempty"`
    // use cert compression
    // todo: is_full_cert -> compressed
    IsFullCert bool `protobuf:"varint,3,opt,name=is_full_cert,json=isFullCert,proto3" json:"is_full_cert,omitempty"`
}

如上所說(shuō),長(zhǎng)安鏈的交易簽名 RequestSignature 這個(gè)字段存的是請(qǐng)求的簽名(包括 HeaderRequestPayload)。RequestPayload是數(shù)據(jù),無(wú)需多言;Header中包括鏈名、簽名證書(shū)、交易ID、時(shí)間戳等信息。簽名證書(shū)的結(jié)構(gòu)是 SerializedMember,使用了證書(shū)壓縮機(jī)制,前面提到過(guò)。

大致上可以說(shuō),長(zhǎng)安鏈中交易包含的信息和 Fabric 是差不多的,唯一的顯著區(qū)別是,F(xiàn)abric 中有nonce,而長(zhǎng)安鏈中沒(méi)有。nonce是密碼學(xué)中一次數(shù),每次需要用nonce的時(shí)候會(huì)隨機(jī)生成一個(gè),由隨機(jī)算法保證每次生成的數(shù)足夠隨機(jī),以至于不會(huì)碰到2個(gè)相同的nonce。

為什么長(zhǎng)安鏈中沒(méi)有nonce,這個(gè)設(shè)計(jì)有些不太合理。賣(mài)個(gè)關(guān)子,下次繼續(xù)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容