长安链介绍-02

    • 长安链设计不好也不坏的地方
      • RwSet中 Key 版本号的设计
      • DAG 的设计
    • 长安链设计不好的地方
      • Gas 的使用
      • 校验身份证书的时机
      • 签名个数的问题
      • 交易到底由谁来签名、对什么签名
      • 交易模型的问题
      • 交易签名没有nonce

长安链设计不好也不坏的地方

再来说说一些长安链设计上比较中性的地方。这些地方可能不见得好,也不见得坏,可能只是在有多种类似选择的时候,选择了其中一种;也可能是一种新特性,但相比其他产品,并没有太多的进步。

  • RwSet中 Key 版本号的设计

Fabric 中的版本号是设计成BlockNumber + Tx Offset in Block的形式,比如,某个Key当前版本号是(3, 2),表示第3个块中的第2条交易最后写入了这个Key。换句话说,Fabric 的版本号是精确到 Tx 的,同一个 Tx 内发生的写入,版本号是一样的。

而长安链的版本号设计成TxID + Key Offset in WriteSet,有些不可思议,这意味着,同一个 Tx 写入的不同 Key 会有不同的版本号,这与常识的认知不符。因为对单一的 Tx,技术上应该是要满足事务属性,即满足“同时成功,或同时失败”,对于有这样特性的系统(如关系数据库和一些NoSQL数据库),我们通常会把同一个 Tx 内的写入标记为相同的版本。长安链做这样的选择明显是不必要的,可以说是一个设计失误。如果想版本号和 TxID 相关,直接设计成“TxID”即可。

我查了一下代码,发现版本号 KeyVersion 这个类在代码中并没有应用,这个“坏设计”暂时没有启用,应该也没有造成什么伤害。

退一步说,长安链即使是设计成只有“TxID”方式,对比 Fabric 的“BlockNumber + Tx Offset in Block”,也不见得就一定有优势。Fabric 这样的设计可以明显看出两个版本号哪一个更加新一点,因为版本号是单调递增的整数;长安链就不行,TxID是随机生成的,无法用来判断版本新旧。这个特性在追溯某个 Key 的变更历史的时候会很方便。

本来,这个特性会被我归类到下一个章节,至少从目前的分析上来看,这个设计毫无可取之处。但没有这样做的原因是,这个版本号设计并没有在代码中应用,万一,我是说万一,也许后面会改进呢?也许会有更好的我们没有想到的特性呢?

  • DAG 的设计

首先要说,长安链的 DAG 和通常理解的区块链里面的 DAG(有向无环图)“不一样”。

通常理解的区块链领域的 DAG 应该是这样,代表项目有 IOTA,ByteBall。DAG 一般表示的是一种区块的组织方式,从数据结构的角度来看,区块链是将区块以链表的形式连接起来的,而 DAG 是将区块以有向无环图的形式连接起来的。这些都是在几年前就提出的概念,不再赘述。

而长安链中的 DAG 则完全不同,它是区块中的一个部分,其作用是整理当前区块中每个交易之间的关系,看是否有前一个交易和后一个交易有依赖关系的情况。当交易之间有依赖关系的时候,调度合约的时候需要按照依赖关系排序执行;如果交易之间彼此没有依赖关系,那么无所谓先后,大家都可以并行执行,可以带来效率的提升。

目前看起来,长安链的这个创新点还是很不错的,应该是原创(不排除有其他项目已经先实现了,只是我不知道),不是上面 Policy 机制、修改链配置那种小幅度的创新。但是,我还是没有把这个特性排到上一章节,因为,不管从实际的运行效果来看,还是设计的对比来看,这个改进对比 Fabric 的 Simulation+Validation 机制,实在很难说更好。

长安链设计不好的地方

  • Gas 的使用

长安链的智能合约运行支持Gas,代码中出现 Gas 的地方很多,比如:

//-- tx_sim_context.gofunc (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.gofunc CreateInstance(contextId int64, code exec.Code, method string, contractId *commonPb.ContractId, gasUsed uint64, gasLimit int64) (*wxvmInstance, error) {

不得不说,这是一个不必要的设计。

Gas 最早应该是出现在以太坊项目中,彼时应该还没有联盟链的概念,所有的区块链都是公有链。以太坊提出了智能合约概念,这个在当时是先进的,但是并没有机制保证所有的智能合约都是“善意的”。例如,如果有一个智能合约,故意写了一个死循环,那么执行这个合约的节点就直接服务宕机了。为了防止这样的情况出现,以太坊引入了 Gas 机制,智能合约中每执行一次操作,都要消耗一定量的 Gas,执行智能合约的时候还需要传入参数 GasLimit,表示此次执行合约所需的 Gas 的上限,如果当使用的 Gas 超出了这个上限,就停止合约执行,将此交易标记为无效。

可见,这个机制是应对公有链网络中的不确定性,而想出来的办法。那在联盟链产品中,这样的特性还是必要的吗?我认为完全不必要。首先,联盟链中的节点相对较少,网络较为封闭,不会对公网开放访问(否则就变成公有链了),在节点受限的情况下,智能合约中有害代码的概率本身就大幅度降低,虽然不可能降低到0。其次,在节点受限的场景下,有很多其他的方案可以作为替代进行合约代码控制,甚至可以加入人为干预的流程,例如,人工智能合约代码审查(不要笑,这个方案现实中很管用);复杂一些的,可以参考 Fabric 的 Endorse 机制和 Chaincode 生命周期管理机制。除此之外,智能合约的引擎中不需要处理 Gas 相关的逻辑,对系统也是一种简化。

我相信,长安链的设计者应该也是认为 Gas 是一个不必要的设计,因为在代码中,所有的 GasLimit 都设置为了一个很大的常数值,

//-- vm_interface.goconst (GasLimit= 1e10// invoke user contract max gasTimeLimit = 1 * 1e9 // 1s

说明,长安链的设计者也认为,不需要使用 Gas 机制来进行控制。Gas 目前还存在在代码里面的原因可能是,智能合约的虚拟机(VM)代码里面本身需要使用 Gas,而VM的代码可能是从已有的公有链代码移植过来的,为了适配旧的VM代码 Gas 机制被留存了。也有另一种可能,将来长安链是不是会有公有链化的可能?这样 Gas 机制就又能用了。

总之,站在联盟链的角度上,Gas 机制无疑是一个坏设计,这是项目中的一个技术债,有可能将来会被解决。项目 v1.0.0 版本就有技术债,感觉不太好。

  • 校验身份证书的时机

作为联盟链的一个重要特性——“准入”,长安链在这一点上做的还有很多不足。因为联盟链的非公有属性,导致其必须提供拒绝非法节点接入的特性,而长安链很多地方忽略了这一点。

比如,区块同步的请求的时候,最初是在这里注册(register)同步请求处理方法,代码如下,

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

sync.net.ReceiveMsg 方法实际是完成一个注册功能,当收到 NetMsg_SYNC_BLOCK_MSG 请求的时候,调用 sync.blockSyncMsgHandler 来处理,而 sync.blockSyncMsgHandler 的代码中,

//-- blockchain_sync_server.gofunc (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 errorsyncMsg = 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)......

进入函数之后就进行一些断言判定,然后反序列化,最后就去执行逻辑功能了。在该接口注册的时候,sync.net.ReceiveMsg 也没有包装一层校验逻辑,来判断请求者的身份是否有“准入”的资格。换言之,长安链在区块同步的时候,没有设置节点接入的门槛,节点上的数据可以被一个模拟的节点,全部同步到链之外的地方。这根本是公有链的性质,而非联盟链。

即使退一步来说,将长安链定位为公有链,那么其共识机制只提供了raft、bft类的机制,也是无法满足公有链的要求。所以,目前长安链实际上处于联盟链和公有链之间的状态,无论作为公有链和联盟链来看,都有较多的不足。

这个问题不止在区块同步的时候有,其他请求也有。当然,这个问题也不是那么难解决,在接入的地方加入身份证书验证即可,这本来就应该是长安链已经提供的 Policy 机制的一部分。

  • 签名个数的问题

下面几个问题都是和 Policy 机制相关的,先来看看第一个问题,交易的签名到底是一个列表还是单一的对象。先看代码,交易类型的定义是这样,

//-- transaction.pb.go// a transaction includes request and its resulttype Transaction struct {// header of the transactionHeader *TxHeader `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`// payload of the requestRequestPayload []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 headerResult *Result `protobuf:"bytes,4,opt,name=result,proto3" json:"result,omitempty"`}

protobuf 生成的代码中看不出来什么,只知道签名 RequestSignature 是一个 byte 数组。再来看使用这个签名的地方,

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

在节点验证签名的时候,签名验证的主入口函数是 verifyTxAuth,这里做了一个非常奇怪的转换,把签名 RequestSignature 转换为只有一个元素的 EndorsementEntry 列表,然后再进行构造身份,身份验证(身份验证用的是之前提到的 Policy 机制)等逻辑处理。

这里我很谨慎的做一个判断:长安链的交易签名只有1个,之前提到的 Policy 机制在这种情况下,几乎无法使用,可能只有 SELF、ANY 能勉强用一下。我做出这个判断的时候我自己也吓了一跳,毕竟长安链引入 Policy 的机制其实也挺麻烦的,但引入之后却没有去用这个机制,这于情于理都无法解释。但在我仔细查找了代码之后,我还是做出了这个判断。

实际上,Policy 机制的实现代码还是很完善的,对不同的 ALL、MAJORITY、ANY、阈值、分数等规则都有处理,但是调用的地方只有一个签名。这说明,长安链在规划中,是有计划将 Policy 机制应用好的,但是在客户端提交交易前构建签名列表的时候,暂时还没有加入多签名的机制。这直接导致了 Policy 机制的残缺,因此只能将这个问题归类到坏设计里面。

  • 交易到底由谁来签名、对什么签名

还是签名的问题,再看一下交易的定义,

//-- transaction.pb.go// a transaction includes request and its resulttype Transaction struct {// header of the transactionHeader *TxHeader `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`// payload of the requestRequestPayload []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 headerResult *Result `protobuf:"bytes,4,opt,name=result,proto3" json:"result,omitempty"`}

注释说的很清楚,签名 RequestSignature 这个字段存的是对请求的签名(包括 HeaderRequestPayload)。为什么是对请求的签名,而不是对交易结果的签名?

Fabric 中的 Policy 机制是对交易结果(而非交易请求)进行签名,在 Fabric 中,交易结果的主要数据结构是读写集(RwSet),这个结果是由不同节点的智能合约执行得到的共同结果,节点通过对结果签名,表示对此结果的背书(Endorsement)。因此,同一条交易才会有多个签名,也因此,才会需要有背书的 Policy 机制来进行验证。对比一下,

  1. 长安链对交易请求数据进行签名;Fabric 对交易结果数据进行签名;
  2. 长安链由交易发起方来签名;Fabric 由交易执行方来签名;
  3. 长安链的签名只有1个;Fabric 的签名可以多个

正因为长安链设计成对交易请求进行签名,所以只能由请求方来签名;正是因为由请求方来签名,而请求方通常只有一方,所以才导致了签名只有1个。通常业务场景中,请求方多数是一方的时候居多。例如,写入订单的场景,发起者就是下单的人,这个操作的请求方只有1个;也有一些需要两方请求的场景,如转账(即使是这个场景也是一方请求,很少双方共同请求);需要三方或以上请求的业务场景就非常罕见了。

这样分析的话,似乎就找到了交易中只有一个签名的原因:长安链在设计 Policy 机制的时候,选择了对交易请求进行签名。也因此导致了其 Policy 机制残缺的现状。

当然,我并不是说 Policy 机制只有像 Fabric 中这样用才是对的,其他用法只要逻辑自洽自然也完全可以,但目前长安链的用法实在很难自圆其说。

综上,长安链应该只是把 Fabric 的 Policy 机制硬套在了其技术架构上面,签名既签错了数据,也由错误的成员来签名,导致了 Policy 机制在长安链里几乎没发挥什么作用。

  • 交易模型的问题

这个问题要说清楚会比较长,先跳过。

  • 交易签名没有nonce

最后说一个密码相关的问题吧,还是交易签名。先对比下 Fabric 有关签名的代码,

//-- transaction.pb.gotype SignatureHeader struct {// Creator of the message, a marshaled msp.SerializedIdentityCreator []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.gopaylBytes := MarshalOrPanic(&common.Payload{Header: MakePayloadHeader(payloadChannelHeader, payloadSignatureHeader),Data: data,},)var sig []byteif signer != nil {sig, err = signer.Sign(paylBytes)if err != nil {return nil, err}}

最后的 sig, err = signer.Sign(paylBytes) 这一行会计算出签名 sig,签名的数据 paylBytes 包括3个部分,ChannelHeaderSignatureHeaderDataData是数据,无需多说;ChannelHeader包括一些链的基本信息,链名、消息版本、时间戳等等,非重点;SignatureHeader包含2个信息,证书和nonce。

再看一下长安链的实现代码,这里需要再次请出交易结构的代码,

//-- transaction.pb.go// a transaction includes request and its resulttype Transaction struct {// header of the transactionHeader *TxHeader `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`// payload of the requestRequestPayload []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 headerResult *Result `protobuf:"bytes,4,opt,name=result,proto3" json:"result,omitempty"`}//-- request.pb.go// header of the requesttype TxHeader struct {// blockchain identifierChainId string `protobuf:"bytes,1,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"`// sender identifierSender *accesscontrol.SerializedMember `protobuf:"bytes,2,opt,name=sender,proto3" json:"sender,omitempty"`// transaction typeTxType 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 uniqueTxId string `protobuf:"bytes,4,opt,name=tx_id,json=txId,proto3" json:"tx_id,omitempty"`// transaction timestamp, in unix timestamp format, secondsTimestamp 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 yetExpirationTime int64 `protobuf:"varint,6,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"`}//-- member.pb.go// Serialized member of blockchaintype SerializedMember struct {// organization identifier of the memberOrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"`// member identity related info bytesMemberInfo []byte `protobuf:"bytes,2,opt,name=member_info,json=memberInfo,proto3" json:"member_info,omitempty"`// use cert compression// todo: is_full_cert -> compressedIsFullCert bool `protobuf:"varint,3,opt,name=is_full_cert,json=isFullCert,proto3" json:"is_full_cert,omitempty"`}

如上所说,长安链的交易签名 RequestSignature 这个字段存的是请求的签名(包括 HeaderRequestPayload)。RequestPayload是数据,无需多言;Header中包括链名、签名证书、交易ID、时间戳等信息。签名证书的结构是 SerializedMember,使用了证书压缩机制,前面提到过。

大致上可以说,长安链中交易包含的信息和 Fabric 是差不多的,唯一的显著区别是,Fabric 中有nonce,而长安链中没有。nonce是密码学中一次数,每次需要用nonce的时候会随机生成一个,由随机算法保证每次生成的数足够随机,以至于不会碰到2个相同的nonce。

为什么长安链中没有nonce? 这个设计有些不太合理。