聊聊长安链的技术设计(二)
继续。
长安链:不好的地方
终于到了不好的地方了,对下面的每一点,我尽量做到“理由充分”,至少能说服我自己吧。
Gas 的使用
长安链的智能合约运行支持Gas,代码中出现 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) {
不得不说,这是一个不必要的设计。
Gas 最早应该是出现在以太坊项目中,彼时应该还没有联盟链的概念,所有的区块链都是公有链。以太坊提出了智能合约概念,这个在当时是先进的,但是并没有机制保证所有的智能合约都是“善意的”。例如,如果有一个智能合约,故意写了一个死循环,那么执行这个合约的节点就直接服务宕机了。为了防止这样的情况出现,以太坊引入了 Gas 机制,智能合约中每执行一次操作,都要消耗一定量的 Gas,执行智能合约的时候还需要传入参数 GasLimit,表示此次执行合约所需的 Gas 的上限,如果当使用的 Gas 超出了这个上限,就停止合约执行,将此交易标记为无效。
可见,这个机制是应对公有链网络中的不确定性,而想出来的办法。那在联盟链产品中,这样的特性还是必要的吗?我认为完全不必要。首先,联盟链中的节点相对较少,网络较为封闭,不会对公网开放访问(否则就变成公有链了),在节点受限的情况下,智能合约中有害代码的概率本身就大幅度降低,虽然不可能降低到0。其次,在节点受限的场景下,有很多其他的方案可以作为替代进行合约代码控制,甚至可以加入人为干预的流程,例如,人工智能合约代码审查(不要笑,这个方案现实中很管用);复杂一些的,可以参考 Fabric 的 Endorse 机制和 Chaincode 生命周期管理机制。除此之外,智能合约的引擎中不需要处理 Gas 相关的逻辑,对系统也是一种简化。
我相信,长安链的设计者应该也是认为 Gas 是一个不必要的设计,因为在代码中,所有的 GasLimit 都设置为了一个很大的常数值,
// -- vm_interface.go
const (
GasLimit = 1e10 // invoke user contract max gas
TimeLimit = 1 * 1e9 // 1s
说明,长安链的设计者也认为,不需要使用 Gas 机制来进行控制。Gas 目前还存在在代码里面的原因可能是,智能合约的虚拟机(VM)代码里面本身需要使用 Gas,而VM的代码可能是从已有的公有链代码移植过来的,为了适配旧的VM代码 Gas 机制被留存了。也有另一种可能,将来长安链是不是会有公有链化的可能?这样 Gas 机制就又能用了。
总之,站在联盟链的角度上,Gas 机制无疑是一个坏设计,这是项目中的一个技术债,有可能将来会被解决。项目 v1.0.0 版本就有技术债,感觉不太好。
校验身份证书的时机
作为联盟链的一个重要特性——“准入”,长安链在这一点上做的还有很多不足。因为联盟链的非公有属性,导致其必须提供拒绝非法节点接入的特性,而长安链很多地方忽略了这一点。
比如,区块同步的请求的时候,最初是在这里注册(register)同步请求处理方法,代码如下,
// -- blockchain_sync_server.go
if 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.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)
......
进入函数之后就进行一些断言判定,然后反序列化,最后就去执行逻辑功能了。在该接口注册的时候,sync.net.ReceiveMsg
也没有包装一层校验逻辑,来判断请求者的身份是否有“准入”的资格。换言之,长安链在区块同步的时候,没有设置节点接入的门槛,节点上的数据可以被一个模拟的节点,全部同步到链之外的地方。这根本是公有链的性质,而非联盟链。
即使退一步来说,将长安链定位为公有链,那么其共识机制只提供了raft、bft类的机制,也是无法满足公有链的要求。所以,目前长安链实际上处于联盟链和公有链之间的状态,无论作为公有链和联盟链来看,都有较多的不足。
这个问题不止在区块同步的时候有,其他请求也有。当然,这个问题也不是那么难解决,在接入的地方加入身份证书验证即可,这本来就应该是长安链已经提供的 Policy 机制的一部分。
签名个数的问题
下面几个问题都是和 Policy 机制相关的,先来看看第一个问题,交易的签名到底是一个列表还是单一的对象。先看代码,交易类型的定义是这样,
// -- 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 生成的代码中看不出来什么,只知道签名 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 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
}
在节点验证签名的时候,签名验证的主入口函数是 verifyTxAuth
,这里做了一个非常奇怪的转换,把签名 RequestSignature
转换为只有一个元素的 EndorsementEntry
列表,然后再进行构造身份,身份验证(身份验证用的是之前提到的 Policy 机制)等逻辑处理。
这里我很谨慎的做一个判断:长安链的交易签名只有1个,之前提到的 Policy 机制在这种情况下,几乎无法使用,可能只有 SELF、ANY 能勉强用一下。我做出这个判断的时候我自己也吓了一跳,毕竟长安链引入 Policy 的机制其实也挺麻烦的,但引入之后却没有去用这个机制,这于情于理都无法解释。但在我仔细查找了代码之后,我还是做出了这个判断。
实际上,Policy 机制的实现代码还是很完善的,对不同的 ALL、MAJORITY、ANY、阈值、分数等规则都有处理,但是调用的地方只有一个签名。这说明,长安链在规划中,是有计划将 Policy 机制应用好的,但是在客户端提交交易前构建签名列表的时候,暂时还没有加入多签名的机制。这直接导致了 Policy 机制的残缺,因此只能将这个问题归类到坏设计里面。
交易到底由谁来签名、对什么签名
还是签名的问题,再看一下交易的定义,
// -- 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"`
}
注释说的很清楚,签名 RequestSignature
这个字段存的是对请求的签名(包括 Header
和RequestPayload
)。为什么是对请求的签名,而不是对交易结果的签名?
Fabric 中的 Policy 机制是对交易结果(而非交易请求)进行签名,在 Fabric 中,交易结果的主要数据结构是读写集(RwSet),这个结果是由不同节点的智能合约执行得到的共同结果,节点通过对结果签名,表示对此结果的背书(Endorsement)。因此,同一条交易才会有多个签名,也因此,才会需要有背书的 Policy 机制来进行验证。对比一下,
- 长安链对交易请求数据进行签名;Fabric 对交易结果数据进行签名;
- 长安链由交易发起方来签名;Fabric 由交易执行方来签名;
- 长安链的签名只有1个;Fabric 的签名可以多个;
正因为长安链设计成对交易请求进行签名,所以只能由请求方来签名;正是因为由请求方来签名,而请求方通常只有一方,所以才导致了签名只有1个。通常业务场景中,请求方多数是一方的时候居多。例如,写入订单的场景,发起者就是下单的人,这个操作的请求方只有1个;也有一些需要两方请求的场景,如转账(即使是这个场景也是一方请求,很少双方共同请求);需要三方或以上请求的业务场景就非常罕见了。
这样分析的话,似乎就找到了交易中只有一个签名的原因:长安链在设计 Policy 机制的时候,选择了对交易请求进行签名。也因此导致了其 Policy 机制残缺的现状。
当然,我并不是说 Policy 机制只有像 Fabric 中这样用才是对的,其他用法只要逻辑自洽自然也完全可以,但目前长安链的用法实在很难自圆其说。
综上,长安链应该只是把 Fabric 的 Policy 机制硬套在了其技术架构上面,签名既签错了数据,也由错误的成员来签名,导致了 Policy 机制在长安链里几乎没发挥什么作用。
交易模型的问题
这个问题要说清楚会比较长,单写一篇文章来说吧,这里先跳过。
交易签名没有nonce
最后说一个密码相关的问题吧,还是交易签名。先对比下 Fabric 有关签名的代码,
// -- 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)
这一行会计算出签名 sig
,签名的数据 paylBytes
包括3个部分,ChannelHeader
、SignatureHeader
和Data
。Data
是数据,无需多说;ChannelHeader
包括一些链的基本信息,链名、消息版本、时间戳等等,非重点;SignatureHeader
包含2个信息,证书和nonce。
再看一下长安链的实现代码,这里需要再次请出交易结构的代码,
// -- 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"`
}
如上所说,长安链的交易签名 RequestSignature
这个字段存的是请求的签名(包括 Header
和RequestPayload
)。RequestPayload
是数据,无需多言;Header
中包括链名、签名证书、交易ID、时间戳等信息。签名证书的结构是 SerializedMember
,使用了证书压缩机制,前面提到过。
大致上可以说,长安链中交易包含的信息和 Fabric 是差不多的,唯一的显著区别是,Fabric 中有nonce,而长安链中没有。nonce是密码学中一次数,每次需要用nonce的时候会随机生成一个,由随机算法保证每次生成的数足够随机,以至于不会碰到2个相同的nonce。
为什么长安链中没有nonce,这个设计有些不太合理。卖个关子,下次继续。