Fabric Block区块结构解析
作者: AlexTan
前言
最近自己在用Fabric-sdk-go
写区块链浏览器,真的觉得Golang作为Hyperledger Fabric的亲儿子语言,但对SDK的支持极不友好,可以说Fabric-sdk-go
几乎没有文档,更多的只能查看源码中的测试用例来使用,而对于测试用例来说,想完成区块链浏览器的需求,还很不够,特别是对Fabric本身的区块及交易的数据结构的解析是完全没有的,比如说:
在获取区块的的测试用例中就返回了一个
*common.Block
结构,而如何处理这个Block,是完全没有的。
而网上对Fabric的区块结构解析有部分内容还是错的,因此这里写一篇博客来帮助大家避免踩坑。
区块结构图
以下两张图是网上找的,第一张图是区块的结构,第二张图是数据结构的定义图,两张图配合看。不过,请注意,我在使用当中发现第二张图数据结构的定义图有部分内容是错误的,不知道是不是版本原因,我用的是1.4.6版本,比如说:
SignatureHeader
下的signingidentity
应该是SerializedIdentity
不知道画图的大神是用的什么版本的Fabric,因此图片仅供大家酌情参考。
好了,废话不多说,放图:
完全解析后的区块数据
{
"data": {
"data": {
"data": [
{
"payload": {
"data": {
"actions": [
{
"header": {
"creator": {
"id_bytes": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNLekNDQWRHZ0F3SUJBZ0lSQUxrUEVNZ1lEZ0lxaXJRMXdtM1ZkaWt3Q2dZSUtvRWN6MVVCZzNVd2N6RUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpFdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekV1WlhoaGJYQnNaUzVqYjIwd0hoY05NakF4TVRFek1USXdNREV6V2hjTk16QXhNVEV4TVRJd01ERXoKV2pCc01Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTgpVMkZ1SUVaeVlXNWphWE5qYnpFUE1BMEdBMVVFQ3hNR1kyeHBaVzUwTVI4d0hRWURWUVFEREJaVmMyVnlNVUJ2CmNtY3hMbVY0WVcxd2JHVXVZMjl0TUZrd0V3WUhLb1pJemowQ0FRWUlLb0VjejFVQmdpMERRZ0FFN0Qza0VDUTAKVUYweUhmQVFsTyt0bEVKT0MzdVU4c0tlTDM4N1NkTjhNMkZLSE9sZmNJSkVPVWhUeUtDMmtUQUpWMzZpTUlhdAo4V2JHMVN2S1ZBcWZTYU5OTUVzd0RnWURWUjBQQVFIL0JBUURBZ2VBTUF3R0ExVWRFd0VCL3dRQ01BQXdLd1lEClZSMGpCQ1F3SW9BZ25pbmsvSWF2UW0yWXJ0cXAyMThZVjFvdXlHem1aZzNEUjBiR3Ezd1VvenN3Q2dZSUtvRWMKejFVQmczVURTQUF3UlFJaEFJeldmZ2NUTVJ4WE5rT1NlSmRoTFNnRmQ1NnU4WkFQS1EvVk9JN2kxeEd5QWlBcAozSituSVZQaFo4QlFSMFdadmJIYUFjN3VKWWRFVXVuYndJUUk0Z1o4YkE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==",
"mspid": "Org1MSP"
},
"nonce": "UIsr0CSC/gLmpdMZDnDmN3pR/T53NH5q"
},
"payload": {
"action": {
"endorsements": [
{
"endorser": "CgdPcmcxTVNQEqYGLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNKakNDQWM2Z0F3SUJBZ0lRSXhGc2RKSDJra0FPSk9lbkJTSjJBekFLQmdncWdSelBWUUdEZFRCek1Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1SUVaeQpZVzVqYVhOamJ6RVpNQmNHQTFVRUNoTVFiM0puTVM1bGVHRnRjR3hsTG1OdmJURWNNQm9HQTFVRUF4TVRZMkV1CmIzSm5NUzVsZUdGdGNHeGxMbU52YlRBZUZ3MHlNREV4TVRNeE1qQXdNVE5hRncwek1ERXhNVEV4TWpBd01UTmEKTUdveEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxVApZVzRnUm5KaGJtTnBjMk52TVEwd0N3WURWUVFMRXdSd1pXVnlNUjh3SFFZRFZRUURFeFp3WldWeU1DNXZjbWN4CkxtVjRZVzF3YkdVdVkyOXRNRmt3RXdZSEtvWkl6ajBDQVFZSUtvRWN6MVVCZ2kwRFFnQUVpVFNRNWVFZU0vancKQkU5UDJJRlh6a3VWVlRKbHI0dnA4NGJ3L3E5V3ZIZzg1NDJSZ0xGa3pNVmllcXY1OEtKRkFyL1MzL1NoNy9naApJN3hmWUJjTmhLTk5NRXN3RGdZRFZSMFBBUUgvQkFRREFnZUFNQXdHQTFVZEV3RUIvd1FDTUFBd0t3WURWUjBqCkJDUXdJb0Fnbmluay9JYXZRbTJZcnRxcDIxOFlWMW91eUd6bVpnM0RSMGJHcTN3VW96c3dDZ1lJS29FY3oxVUIKZzNVRFJnQXdRd0lmUXdwR0FvaWlleUw3R2IyM1FwS2JGYk9GMkZBSWZ2bWhiZ0hIMGgrRHJnSWdUc1hIVGU4VApZNnkxLzVtNW1kcXUzcVMxSTJyZFNhQjhsRTBEZGpEOWxVcz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=",
"signature": "MEUCIHKOMG//auILmgI7qEehkWjjwZZmnLOQjf+KXeS/3rUwAiEA1BUOnwyJjT6npM9C9UIdaORnkXH0/QTkdu6oR0nuRr8="
}
],
"proposal_response_payload": {
"extension": {
"chaincode_id": {
"name": "mycc",
"path": "",
"version": "v1"
},
"events": {
"chaincode_id": "mycc",
"event_name": "addCredit",
"payload": null,
"tx_id": "93828232e69cc0614ced9001c06b586c3ccc56ce42c23a3528472c1ed00c9748"
},
"response": {
"message": "",
"payload": "5L+h5oGv5re75Yqg5oiQ5Yqf",
"status": 200
},
"results": {
"data_model": "KV",
"ns_rwset": [
{
"collection_hashed_rwset": [],
"namespace": "lscc",
"rwset": {
"metadata_writes": [],
"range_queries_info": [],
"reads": [
{
"key": "mycc",
"version": {
"block_num": "1",
"tx_num": "0"
}
}
],
"writes": []
}
},
{
"collection_hashed_rwset": [],
"namespace": "mycc",
"rwset": {
"metadata_writes": [],
"range_queries_info": [],
"reads": [
{
"key": "test3",
"version": null
}
],
"writes": [
{
"is_delete": false,
"key": "test3",
"value": "eyJkb2NUeXBlIjoiQ3JlZGl0T2JqIiwiWnhIYXNoIjoiYWUyYzdkNjUyZTc2ZTU3NjcxZjhmZjA1OGQxOTliODU3OGEyNzJmZWY1NmM3ZWRiMTVjZjllODM3ODkzMGM5NyIsIk9wZXJhdG9yIjoi5rWL6K+VMSIsIlRpbWUiOiIyMDIwLTEyLTAzIDEyOjIyOjAzIiwiUGRmSWQiOiJ0ZXN0MyIsInNhdmVfcGF0aCI6IiIsInNxbF92YWx1ZSI6eyLkv6HnlKjor4TnuqciOiJBIiwi5b6X5YiGIjoiMTAifSwiSGlzdG9yaWVzIjpudWxsfQ=="
}
]
}
}
]
}
},
"proposal_hash": "aNwzCaemR8oP8MG/74cCpDa0G1ekmrBOl5wCZ8tawaU="
}
},
"chaincode_proposal_payload": {
"TransientMap": {},
"input": {
"chaincode_spec": {
"chaincode_id": {
"name": "mycc",
"path": "",
"version": ""
},
"input": {
"args": [
"YWRkQ3JlZGl0",
"eyJkb2NUeXBlIjoiIiwiWnhIYXNoIjoiYWUyYzdkNjUyZTc2ZTU3NjcxZjhmZjA1OGQxOTliODU3OGEyNzJmZWY1NmM3ZWRiMTVjZjllODM3ODkzMGM5NyIsIk9wZXJhdG9yIjoi5rWL6K+VMSIsIlRpbWUiOiIyMDIwLTEyLTAzIDEyOjIyOjAzIiwiUGRmSWQiOiJ0ZXN0MyIsInNhdmVfcGF0aCI6IiIsInNxbF92YWx1ZSI6eyLkv6HnlKjor4TnuqciOiJBIiwi5b6X5YiGIjoiMTAifSwiSGlzdG9yaWVzIjpudWxsfQ=="
],
"decorations": {},
"is_init": false
},
"timeout": 0,
"type": "UNDEFINED"
}
}
}
}
}
]
},
"header": {
"channel_header": {
"channel_id": "mychannel",
"epoch": "0",
"extension": {
"chaincode_id": {
"name": "mycc",
"path": "",
"version": ""
}
},
"timestamp": "2020-12-03T12:22:03.799607788Z",
"tls_cert_hash": null,
"tx_id": "93828232e69cc0614ced9001c06b586c3ccc56ce42c23a3528472c1ed00c9748",
"type": 3,
"version": 0
},
"signature_header": {
"creator": {
"id_bytes": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNLekNDQWRHZ0F3SUJBZ0lSQUxrUEVNZ1lEZ0lxaXJRMXdtM1ZkaWt3Q2dZSUtvRWN6MVVCZzNVd2N6RUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpFdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekV1WlhoaGJYQnNaUzVqYjIwd0hoY05NakF4TVRFek1USXdNREV6V2hjTk16QXhNVEV4TVRJd01ERXoKV2pCc01Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTgpVMkZ1SUVaeVlXNWphWE5qYnpFUE1BMEdBMVVFQ3hNR1kyeHBaVzUwTVI4d0hRWURWUVFEREJaVmMyVnlNVUJ2CmNtY3hMbVY0WVcxd2JHVXVZMjl0TUZrd0V3WUhLb1pJemowQ0FRWUlLb0VjejFVQmdpMERRZ0FFN0Qza0VDUTAKVUYweUhmQVFsTyt0bEVKT0MzdVU4c0tlTDM4N1NkTjhNMkZLSE9sZmNJSkVPVWhUeUtDMmtUQUpWMzZpTUlhdAo4V2JHMVN2S1ZBcWZTYU5OTUVzd0RnWURWUjBQQVFIL0JBUURBZ2VBTUF3R0ExVWRFd0VCL3dRQ01BQXdLd1lEClZSMGpCQ1F3SW9BZ25pbmsvSWF2UW0yWXJ0cXAyMThZVjFvdXlHem1aZzNEUjBiR3Ezd1VvenN3Q2dZSUtvRWMKejFVQmczVURTQUF3UlFJaEFJeldmZ2NUTVJ4WE5rT1NlSmRoTFNnRmQ1NnU4WkFQS1EvVk9JN2kxeEd5QWlBcAozSituSVZQaFo4QlFSMFdadmJIYUFjN3VKWWRFVXVuYndJUUk0Z1o4YkE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==",
"mspid": "Org1MSP"
},
"nonce": "UIsr0CSC/gLmpdMZDnDmN3pR/T53NH5q"
}
}
},
"signature": "MEYCIQCei8fnd77l3iL0RY2wVwy6SxcM2MK9GWYV/GwbSEFSHAIhAJDqh8BhDczPnbcY0yhfNZ2bI9i7z9V9xl+zdPL9QUJk"
}
]
},
"header": {
"data_hash": "QR8c9tG83W4/8+4jl8W15GPGhV5a8gOhWgFLWpqPUMY=",
"number": "6",
"previous_hash": "ZsUXUM2pphlG/CxdUzFdIvwF38ioc3UEBSbPeae0e8s="
},
"metadata": {
"metadata": [
"Cg8KABILCgkKAwECAxAEGAoSlwcKygYKrQYKCk9yZGVyZXJNU1ASngYtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQ0lEQ0NBY2FnQXdJQkFnSVJBUGFPN1pkZ0I4Z0UyZ2wzWkFLNHJtOHdDZ1lJS29FY3oxVUJnM1V3YVRFTApNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQWdUQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjVERWTmhiaUJHCmNtRnVZMmx6WTI4eEZEQVNCZ05WQkFvVEMyVjRZVzF3YkdVdVkyOXRNUmN3RlFZRFZRUURFdzVqWVM1bGVHRnQKY0d4bExtTnZiVEFlRncweU1ERXhNVE14TWpBd01UTmFGdzB6TURFeE1URXhNakF3TVROYU1Hc3hDekFKQmdOVgpCQVlUQWxWVE1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGhNUll3RkFZRFZRUUhFdzFUWVc0Z1JuSmhibU5wCmMyTnZNUkF3RGdZRFZRUUxFd2R2Y21SbGNtVnlNUjB3R3dZRFZRUURFeFJ2Y21SbGNtVnlNaTVsZUdGdGNHeGwKTG1OdmJUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxQkhNOVZBWUl0QTBJQUJKS2hJazQ5MlVRU2w1WmdVQVN1RlJUZApZOVpVZEpPQ2hBU2E2U2Qxc1VWUjd5QWVzcDY4bUFzQVQ3VGZib3B2UzdjeGpHRm9jYkloSkZjK1liaVBTdENqClRUQkxNQTRHQTFVZER3RUIvd1FFQXdJSGdEQU1CZ05WSFJNQkFmOEVBakFBTUNzR0ExVWRJd1FrTUNLQUlFT0MKYWRIY1ZoaHhZS1dUMEl6RUtaZmhBMmtpQkNrS0RuZmJhVXJaSWJ4cE1Bb0dDQ3FCSE05VkFZTjFBMGdBTUVVQwpJQis5WGY5V25la0R6aFlNNnk3dUwrZ0pMMlZtcTN1TEMrQjBmMk1vUnFXMUFpRUE5MFd5UGd2THEvVDI4Q3JLClZYaDdxWFV2dXQwTkZScVRSTEpYMW03U2VZYz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQoSGCYqepNKdDjThpB0OuphLSYvkJ4FtxNMlxJIMEYCIQClr0fyIVcE7eHjE5Z+2KFJpKf8uHtyyKt9Skf3PyFnawIhAKp3PJJxc3NQUBNgnzp2Jwtr0Ycgg7S+IbHU7OSzAeQR",
"",
"AA==",
"CgkKAwECAxAEGAo=",
"CiDRjMKMZ4dC+S+zBfjclXL4PUxnyvJaVbXSZSavufdfrg=="
]
}
},
"msg": "Get block success",
"status": 1
}
很明显,这个数据并不是我们想要的数据,里面有太多的[]byte
和一堆对区块链浏览器而言无用的字段,而如何解析这些字段,请看下面内容。
注意:建议在做解析字段时结合json数据和图片看
解析区块数据
接下来,我将演示用Go来提取Hyperledger Fabric中块结构的各个部分。
先来看数据结构:
Block
众所周知,区块结构一般分为区块头和body两部分(除此之外还有其他的一些配置数据),Fabric也不例外,Header为Fabric的区块头,Data为Fabric的Body。
type Block struct {
Header *BlockHeader
Data *BlockData
Metadata *BlockMetadata
}
BlockHeader
区块头所包含的信息和bitcoin差不多,包含了区块序列号、前一个区块的哈希和当前区块数据(所有交易)的哈希。注意:DataHash并不是当前区块哈希,当前区块哈希的计算方式为区块头的三个字段(即number、previous_hash、data_hash)首先使用ASN.1中的DER编码规则进行编码,而后进行SHA256哈希值计算得出。
type BlockHeader struct {
Number uint64
PreviousHash []byte
DataHash []byte
}
BlockData
区块Body中只有一个Data字段,它是一个字节数组类型,可以用
common.Envelope
来解析,Envelope
就是一个比较复杂的数据类型了。
type BlockData struct {
Data [][]byte
}
BlockMetaData
MetaData字段包含块的创建时间,证书详细信息和块编写器的签名。这个字段在区块链浏览器中一般用不到,在这里就不做详细解析了。
type BlockMetadata struct {
Metadata [][]byte
}
Envelope
提取区块中的交易数据就得从此数据结构开始。
type Envelope struct {
Payload []byte
Signature []byte
}
从BlockData中获取Envelope
func GetEnvelopeFromBlock(data []byte) (*common.Envelope, error){
var err error
env := &common.Envelope{}
if err = proto.Unmarshal(data, env); err != nil {
return nil, errors.Wrap(err, "error unmarshaling Envelope")
}
return env, nil
}
Payload
从完全解析成json后的区块数据中可以看出,
Palyload
中包含了Header
和Data
两个变量,其中Header中包含了ChannelHeader
和SignatureHeader
。
type Payload struct {
Header *Header
Data []byte
}
从Envelop中获取Payload
payload := &common.Payload{}
err = proto.Unmarshal(envelope.Payload, payload)
if err != nil {
return errors.WithMessage(err,"unmarshaling Payload error: ")
}
Payload Header
ChannelHeader
包含了channel信息和链码信息
type ChannelHeader struct {
Type int32
Version int32
Timestamp *google_protobuf.Timestamp
ChannelId string
TxId string
Epoch uint64
Extension []byte
}
/*
Type: It denotes the transaction type used. ["MESSAGE", "CONFIG", "CONFIG_UPDATE", "ENDORSER_TRANSACTION",
"ORDERER_TRANSACTION", "DELIVER_SEEK_INFO", "CHAINCODE_PACKAGE"]
Version: The version number for protobuf used for serialization/de-serialization of the structures.
ChannelId: Channel name for the network
TxId: The transaction id for processing the transaction
Epoch: The fields are currently unused.
Extension: It contents Chaincode information that marshalled the ChaincodeHeaderExtension structure.
*/
从Payload中获取ChannelHeader
channelHeader := &common.ChannelHeader{}
err := proto.Unmarshal(payload.Header.ChannelHeader, channelHeader)
if err != nil {
return ChannelHeader{}, errors.WithMessage(err,"unmarshaling Channel Header error: ")
}
SignatureHeader
该结构包含创建者详细信息,该详细信息包含Mspid(成员服务提供者的身份证书)。
type SignatureHeader struct {
Creator []byte
Nonce []byte
}
其中Creator可通过
msp.SerializedIdentity
解析。
从Payload中获取SignatureHeader
signatureHeader := &common.SignatureHeader{}
err := proto.Unmarshal(payload.Header.SignatureHeader, signatureHeader)
if err != nil {
return SignatureHeader{}, errors.WithMessage(err,"unmarshaling Signature Header error: ")
}
从SignatureHeader中获取Creator
type SerializedIdentity struct {
Mspid string
IdBytes []byte
}
creator := &msp.SerializedIdentity{}
err = proto.Unmarshal(signatureHeader.Creator, creator)
if err != nil {
return SignatureHeader{}, errors.WithMessage(err,"unmarshaling Creator error: ")
}
从Creator中解析IdBytes(MSP)
uEnc := base64.URLEncoding.EncodeToString([]byte(creator.IdBytes))
certText, err := base64.URLEncoding.DecodeString(uEnc)
if err != nil {
return SignatureHeader{}, errors.WithMessage(err,"Error decoding string: ")
}
end, _ := pem.Decode([]byte(string(certText)))
if end == nil {
return SignatureHeader{}, errors.New("Error Pem decoding: ")
}
cert, err := x509.ParseCertificate(end.Bytes)
if err != nil {
return SignatureHeader{}, errors.New("failed to parse certificate:: ")
}
certificateJson := Certificate{
Country: cert.Issuer.Country,
Organization: cert.Issuer.Organization,
OrganizationalUnit: cert.Issuer.OrganizationalUnit,
Locality: cert.Issuer.Locality,
Province: cert.Issuer.Province,
SerialNumber: cert.Issuer.SerialNumber,
NotBefore: cert.NotBefore,
NotAfter: cert.NotAfter,
}
Payload Data
包含了区块中的所有交易数据及链码调用及响应数据
TransactionAction
type TransactionAction struct {
Header []byte
Payload []byte
}
Header: 和
SignatureHeader
差不多,包含了用于提交交易的身份详细信息Payload: 可以通过
ChainCodeActionPayload
解析
ChainCodeActionPayload
type ChaincodeActionPayload struct {
ChaincodeProposalPayload []byte
Action *ChaincodeEndorsedAction
}
ChaincodeProposalPayload
包含了调用chaincode时的输入参数等
type ChaincodeProposalPayload struct {
Input []byte
TransientMap map[string][]byte
}
Input: 可以通过
ChaincodeInvocationSpec
解析
ChaincodeInvocationSpec
包含链码信息和调用期间使用的参数。
type ChaincodeInvocationSpec struct {
ChaincodeSpec *ChaincodeSpec
}
type ChaincodeSpec struct {
Type ChaincodeSpec_Type
ChaincodeId *ChaincodeID
Input *ChaincodeInput
Timeout int32
}
type ChaincodeInput struct {
Args [][]byte
Decorations map[string][]byte
}
Action
可通过
ChaincodeEndorsedAction
解析,包含Proposal Hash
及调用链码时的Read/Write的交易信息
type ChaincodeEndorsedAction struct {
ProposalResponsePayload []byte
Endorsements []*Endorsement
}
type ProposalResponsePayload struct {
ProposalHash []byte
Extension []byte
}
Extension可通过
ChaincodeAction
来解析,包含了Read/Write操作的交易
Read/Write Set
type ChaincodeAction struct {
Results []byte
Events []byte
Response *Response
}
Results
可通过
TxReadWriteSet
解析
type KVRWSet struct {
Reads []*KVRead
RangeQueriesInfo []*RangeQueryInfo
Writes []*KVWrite
MetadataWrites []*KVMetadataWrite
}
type KVRead struct {
Key string
Version *Version
}
type Version struct {
BlockNum uint64
TxNum uint64
}
type KVWrite struct {
Key string
IsDelete bool
Value []byte
}
type RangeQueryInfo struct {
StartKey string
EndKey string
ItrExhausted bool
ReadsInfo isRangeQueryInfo_ReadsInfo
}
type KVMetadataWrite struct {
Key string
Entries []*KVMetadataEntry
}
type KVMetadataEntry struct {
Name string
Value []byte
}
结语
如果是做区块链浏览器,上述解析已经足够了,可根据具体情况做适应调整。