id:BSN_2021

公众号:BSN研习社 作者:红枣科技高晨曦

背景:BSN公网Fabric联盟链的出现降低了使用区块链的难度,在通过BSN城市节点网关发起交易时,只能获取最基本交易信息,想要展示更多区块链特性的数据就需要从账本数据中获取,而解析账本数据有一定的难度。

目标:了解账本数据结构,更好的设计开发自己的项目

对象: 使用BSN联盟链Fabric的开发人员

前言

开始之前先看一个简单的合约代码

import (
"github.com/hyperledger/fabric-contract-api-go/contractapi"
"github.com/hyperledger/fabric-contract-api-go/metadata"
"math/big"
)

type DemoChaincode struct {
contractapi.Contract
}

func (d *DemoChaincode) Set(ctx contractapi.TransactionContextInterface, key string, value int64) (string, error) {
bigValue := new(big.Int).SetInt64(value)
keyValue ,err :=ctx.GetStub().GetState(key)
var resultValue string

if err != nil || len(keyValue) ==0 {
keyValue = bigValue.Bytes()
resultValue = bigValue.String()
}else {
bigKeyValue := new(big.Int).SetBytes(keyValue)
result := new(big.Int).Add(bigKeyValue,bigValue)
keyValue = result.Bytes()
resultValue = result.String()
}

err = ctx.GetStub().PutState(key, keyValue)
if err != nil {
return "", err
} else {
ctx.GetStub().SetEvent("set_key",bigValue.Bytes())
return resultValue, nil
}
}

func (d *DemoChaincode) Query(ctx contractapi.TransactionContextInterface, key string) (string, error) {
valueBytes, err := ctx.GetStub().GetState(key)
if err != nil || len(valueBytes) ==0 {
return "0", nil
}
bigKeyValue := new(big.Int).SetBytes(valueBytes)
return bigKeyValue.String(), nil
}

这是一个通过Go语言开发的Fabric智能合约,合约提供了两个合约方法​​Set​​、​​Query​​。

​Set​​方法的主要功能就是根据输入的​​key​​ 读取账本数据并且累加​​value​​的值,并输出结果。

​Query​​方法主要就是查询当前​​key​​的值。

那么我们在调用这个合约的​​Set​​方法时,会在链上留下什么数据,都包含那些有用的信息呢?

接下来我们通过BSN城市节点网关查询交易所在的块信息来查看一个账本的块数据都有那些信息。

通过BSN城市节点网关查询块数据

在此之前,我们先通过接口调用一下​​Set​​方法,拿到合约的执行结果和交易Id。

  cli :=getTestNetFabricClient(t)
nonce,_ :=common.GetRandomNonce()
reqData :=node.TransReqDataBody{
Nonce: base64.StdEncoding.EncodeToString(nonce),
ChainCode: "cc_f73a60f601654467b71bdc28b8f16033",
FuncName: "Set",
Args: []string{"abc","76"},
}

res,err :=cli.ReqChainCode(reqData)
if err !=nil {
t.Fatal(err)
}
resBytes,_ :=json.Marshal(res)
fmt.Println(string(resBytes))

getTestNetFabricClient 方法主要是根据网关地址、用户信息等参数创建了一个FabricClient对象

响应结果为:

{
"header":{
"code":0,
"msg":"success"
},
"mac":"MEUCIQCLnU5gTu6NvM0zn4HH1lDSEef5i6HgNjKS2YRirDfYVgIgJaN+BQRUulS6jtqePAvb/Z3E9U0W5Go4aV7ffrkMbBc=",
"body":{
"blockInfo":{
"txId":"32a4ce2060e4138e6465f907579a9765d50bdb6e89763b064d69664bc4ebf999",
"blockHash":"",
"status":0
},
"ccRes":{
"ccCode":200,
"ccData":"276"
}
}
}

在返回的结果中我们可以看到本次交易的Id为​​32a4ce2060e4138e6465f907579a9765d50bdb6e89763b064d69664bc4ebf999​​,合约的返回结果为​​276​​,状态为​​200​​ 表示合约执行成功。

由于目前BSN网关的​​ReqChainCode​​接口不会等待交易落块后再返回,所以接口不会返回当前交易的块信息,也无法确认交易的最终状态,所以需要我们再次调用接口查询交易的信息。


调用接口查询块信息

BSN城市节点网关提供了两个接口查询块信息 ​​getBlockInfo​​以及​​getBlockData​​,这两个接口的参数相同,返回又有哪些不同呢?

​getBlockInfo​​ 接口返回了解析之后的块信息以及交易的简略信息,只包含了交易Id、状态、提交者、交易时间等。

​getBlockData​​ 接口则返回了完成的块数据,块数据是以​​base64​​编码后的块数据,示例如下:


  txId :="32a4ce2060e4138e6465f907579a9765d50bdb6e89763b064d69664bc4ebf999"
cli :=getTestNetFabricClient(t)

_,block,err :=cli.GetBlockData(node.BlockReqDataBody{TxId:txId })
if err !=nil {
t.Fatal(err)
}

响应结果为:

{
"header":{
"code":0,
"msg":"success"
},
"mac":"MEQCIDIg/lhMy2yK1oaK/7naISwmL9gEYUtVHsgYykUYr73jAiALQcEsIBfmeFZvdgq4gEBNY/thLO/ZJUb/tbPl9ql9WA==",
"body":{
"blockHash":"66e7d01b102a0bbd2ebe55fff608d46512c3d243dde0d1305fec44a31a800932",
"blockNumber":384187,
"preBlockHash":"0ce4a7200bb67aea157e92f8dfea5c40bd1a6390d28cc70bad91e9af79098df4",
"blockData":"Ckg ... muf1s="
}
}

在网关返回的参数中​​blockData​​即完整的块数据。

转换块数据

网关响应的块数据即​​github.com\hyperledger\fabric-protos-go\common\common.pb.go​​中​​Block​​经过​​proto.Marshal​​序列化后的数据进行​​base64​​编码后的。

解析代码如下

func ConvertToBlock(blockData string) (*common.Block, error) {

blockBytes, err := base64.StdEncoding.DecodeString(blockData)
if err != nil {
return nil, errors.WithMessage(err, "convert block data has error")
}
block := &common.Block{}

err = proto.Unmarshal(blockBytes, block)
if err != nil {
return nil, errors.WithMessage(err, "convert block bytes has error")
}

return block, nil
}

同时在​​github.com/hyperledger/fabric-config/protolator​​包中也提供了转换为​​json​​格式的方法:

func ConvertBlockToJson(block *common.Block) (string, error) {
var sb strings.Builder
err := protolator.DeepMarshalJSON(&sb, block)
if err != nil {
return "", err
}
return sb.String(), err
}

Fabric 块内数据包含哪些内容

我们先来看​​common.Block​​ 对象:

// This is finalized block structure to be shared among the orderer and peer
// Note that the BlockHeader chains to the previous BlockHeader, and the BlockData hash is embedded
// in the BlockHeader. This makes it natural and obvious that the Data is included in the hash, but
// the Metadata is not.
type Block struct {
Header *BlockHeader `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
Data *BlockData `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
Metadata *BlockMetadata `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}

下面我们来详细的解释每一部分的内容

Header

// BlockHeader is the element of the block which forms the block chain
// The block header is hashed using the configured chain hashing algorithm
// over the ASN.1 encoding of the BlockHeader
type BlockHeader struct {
Number uint64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"`
PreviousHash []byte `protobuf:"bytes,2,opt,name=previous_hash,json=previousHash,proto3" json:"previous_hash,omitempty"`
DataHash []byte `protobuf:"bytes,3,opt,name=data_hash,json=dataHash,proto3" json:"data_hash,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}

​BlockHeader​​中包含块号、上一个块哈希、当前块的数据哈希。由于不包含当前块的块哈希,所以我们如果想获取当前块的哈希,需要自己手动计算。

即计算​​asn1​​编码后的块号、上一个块哈希、数据哈希的哈希即可。

func GetBlockHASH(info *common.Block) []byte {
asn1Header := asn1Header{
PreviousHash: info.Header.PreviousHash,
DataHash: info.Header.DataHash,
Number: int64(info.Header.Number),
}

result, _ := asn1.Marshal(asn1Header)
return Hash(result)
}

Data

type BlockData struct {
Data [][]byte `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}

​BlockData​​中包含的即为当前块内的每一条交易,是​​common.Envelope​​对象的序列化结果的合集。即每一个交易。

// Envelope wraps a Payload with a signature so that the message may be authenticated
type Envelope struct {
// A marshaled Payload
Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"`
// A signature by the creator specified in the Payload header
Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}

​common.Envelope​​对象即客户端收集到足够的背书结果后向orderer提交的交易内容,包含详细交易以及客户端签名,将​​Payload​​序列化为​​common.Payload​​对象后我么可以得到向orderer提交的channel信息,提交者信息。他们在​​common.Payload.Header​​中。

type Header struct {
ChannelHeader []byte `protobuf:"bytes,1,opt,name=channel_header,json=channelHeader,proto3" json:"channel_header,omitempty"`
SignatureHeader []byte `protobuf:"bytes,2,opt,name=signature_header,json=signatureHeader,proto3" json:"signature_header,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}

他们分别是​​common.ChannelHeader​​以及​​common.SignatureHeader​​对象,这些信息中包含了channelId,交易Id,交易时间,提交者,MSPId,等信息。

而对​​common.Payload.Data​​解析后我们可以得到​​peer.Transaction​​对象。这里就是我们向各个节点发起的交易提案以及节点的背书结果。每一个​​peer.TransactionAction​​对象中包含两部分数据,

// TransactionAction binds a proposal to its action.  The type field in the
// header dictates the type of action to be applied to the ledger.
type TransactionAction struct {
// The header of the proposal action, which is the proposal header
Header []byte `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
// The payload of the action as defined by the type in the header For
// chaincode, it's the bytes of ChaincodeActionPayload
Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}

​Header​​是交易提案提交者身份信息,一般和​​common.Payload.Header​​中的​​SignatureHeader​​一致。

​Payload​​是​​peer.ChaincodeActionPayload​​对象,包含交易提案的详细信息以及节点的模拟执行结果和背书信息。

// ChaincodeActionPayload is the message to be used for the TransactionAction's
// payload when the Header's type is set to CHAINCODE. It carries the
// chaincodeProposalPayload and an endorsed action to apply to the ledger.
type ChaincodeActionPayload struct {
// This field contains the bytes of the ChaincodeProposalPayload message from
// the original invocation (essentially the arguments) after the application
// of the visibility function. The main visibility modes are "full" (the
// entire ChaincodeProposalPayload message is included here), "hash" (only
// the hash of the ChaincodeProposalPayload message is included) or
// "nothing". This field will be used to check the consistency of
// ProposalResponsePayload.proposalHash. For the CHAINCODE type,
// ProposalResponsePayload.proposalHash is supposed to be H(ProposalHeader ||
// f(ChaincodeProposalPayload)) where f is the visibility function.
ChaincodeProposalPayload []byte `protobuf:"bytes,1,opt,name=chaincode_proposal_payload,json=chaincodeProposalPayload,proto3" json:"chaincode_proposal_payload,omitempty"`
// The list of actions to apply to the ledger
Action *ChaincodeEndorsedAction `protobuf:"bytes,2,opt,name=action,proto3" json:"action,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}

​ChaincodeProposalPayload​​存储了​​peer.ChaincodeProposalPayload​​对象的序列化数据,其中包含了向节点发起的交易提案内容,它包含了我们调用的合约信息、合约方以及输入参数等信息。

​ChaincodeEndorsedAction​​对象中包含了两部分数据:

// ChaincodeEndorsedAction carries information about the endorsement of a
// specific proposal
type ChaincodeEndorsedAction struct {
// This is the bytes of the ProposalResponsePayload message signed by the
// endorsers. Recall that for the CHAINCODE type, the
// ProposalResponsePayload's extenstion field carries a ChaincodeAction
ProposalResponsePayload []byte `protobuf:"bytes,1,opt,name=proposal_response_payload,json=proposalResponsePayload,proto3" json:"proposal_response_payload,omitempty"`
// The endorsement of the proposal, basically the endorser's signature over
// proposalResponsePayload
Endorsements []*Endorsement `protobuf:"bytes,2,rep,name=endorsements,proto3" json:"endorsements,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}

​Endorsements​​是节点的背书信息,包含节点证书以及节点签名。

​ProposalResponsePayload​​ 存储了​​peer.ProposalResponsePayload​​对象的序列化数据,它包含了​​ProposalHash​​以及​​Extension​​,

其中​​Extension​​部分的数据在合约调用中为​​peer.ChaincodeAction​​对象的序列化数据,包含了执行的合约名称、合约中对账本的读写集合,合约的返回结果,以及合约事件等。

// ChaincodeAction contains the actions the events generated by the execution
// of the chaincode.
type ChaincodeAction struct {
// This field contains the read set and the write set produced by the
// chaincode executing this invocation.
Results []byte `protobuf:"bytes,1,opt,name=results,proto3" json:"results,omitempty"`
// This field contains the events generated by the chaincode executing this
// invocation.
Events []byte `protobuf:"bytes,2,opt,name=events,proto3" json:"events,omitempty"`
// This field contains the result of executing this invocation.
Response *Response `protobuf:"bytes,3,opt,name=response,proto3" json:"response,omitempty"`
// This field contains the ChaincodeID of executing this invocation. Endorser
// will set it with the ChaincodeID called by endorser while simulating proposal.
// Committer will validate the version matching with latest chaincode version.
// Adding ChaincodeID to keep version opens up the possibility of multiple
// ChaincodeAction per transaction.
ChaincodeId *ChaincodeID `protobuf:"bytes,4,opt,name=chaincode_id,json=chaincodeId,proto3" json:"chaincode_id,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}

​Results​​为合约执行过程中对不同合约的读写信息,由​​rwset.TxReadWriteSet​​对象序列化。

​Events​​为合约执行中的事件信息,有​​peer.ChaincodeEvent​​对象序列化。

​Response​​为合约的返回信息,包含​​Status​​、​​Message​​、​​Payload​​。

​ChaincodeId​​为执行的合约信息,包含合约名,版本等。


Metadata

​Metadata​​ 目前包含5个Byte数组,他们分别对应,Orderer签名信息,最后一个配置块号,交易过滤器,Orderer信息,提交哈希。其中 配置块号和Orderer信息在目前的版本中未使用。

其中交易过滤器中的每一个Bytes表示对应交易的状态信息,转换为​​peer.TxValidationCode​​对象即可。


结语

通过对块数据的学习我们可以查到交易在提案、背书、提交各个阶段的信息,帮助我们更好的理解交易的流程以及每一个参与交易的合约和账本的数据修改情况。对在排查异常交易的过程中提供帮助。 最后用一个块的​​json​​格式数据来帮助大家更好的理解Fabric账本的块数据

{
"data": {
"data": [
{
"payload": {
"data": {
"actions": [
{
"header": {
"creator": {
"id_bytes": "LS0 ... LQo=",
"mspid": "ECDSARTestNodeMSP"
},
"nonce": "rG24c6sj28YGtCo8PBeQMTJsgPusft6m"
},
"payload": {
"action": {
"endorsements": [
{
"endorser": "ChF ... LQo=",
"signature": "MEQCIDr+a5HiELJq1M2vZWc2NqNxDRnCEck7EtErgbvfe+mOAiAx9XKRmCcM2xyEyYoz5l6wMuYE4zDIR5GVvLnz0MAmXg=="
}
],
"proposal_response_payload": {
"extension": {
"chaincode_id": {
"name": "cc_f73a60f601654467b71bdc28b8f16033",
"path": "",
"version": "1.0.0.1"
},
"events": {
"chaincode_id": "cc_f73a60f601654467b71bdc28b8f16033",
"event_name": "set_key",
"payload": "TA==",
"tx_id": "32a4ce2060e4138e6465f907579a9765d50bdb6e89763b064d69664bc4ebf999"
},
"response": {
"message": "",
"payload": "Mjc2",
"status": 200
},
"results": {
"data_model": "KV",
"ns_rwset": [
{
"collection_hashed_rwset": [],
"namespace": "_lifecycle",
"rwset": {
"metadata_writes": [],
"range_queries_info": [],
"reads": [
{
"key": "namespaces/fields/cc_f73a60f601654467b71bdc28b8f16033/Sequence",
"version": {
"block_num": "384158",
"tx_num": "0"
}
}
],
"writes": []
}
},
{
"collection_hashed_rwset": [],
"namespace": "cc_f73a60f601654467b71bdc28b8f16033",
"rwset": {
"metadata_writes": [],
"range_queries_info": [],
"reads": [
{
"key": "abc",
"version": {
"block_num": "384179",
"tx_num": "0"
}
}
],
"writes": [
{
"is_delete": false,
"key": "abc",
"value": "ARQ="
}
]
}
}
]
}
},
"proposal_hash": "3jOb59oJFGtq2NM4loU4cwmHSqp//YV7EwA+qNKV4fo="
}
},
"chaincode_proposal_payload": {
"TransientMap": {},
"input": {
"chaincode_spec": {
"chaincode_id": {
"name": "cc_f73a60f601654467b71bdc28b8f16033",
"path": "",
"version": ""
},
"input": {
"args": [
"U2V0",
"YWJj",
"NzY="
],
"decorations": {},
"is_init": false
},
"timeout": 0,
"type": "GOLANG"
}
}
}
}
}
]
},
"header": {
"channel_header": {
"channel_id": "channel202010310000001",
"epoch": "0",
"extension": {
"chaincode_id": {
"name": "cc_f73a60f601654467b71bdc28b8f16033",
"path": "",
"version": ""
}
},
"timestamp": "2022-06-09T03:29:47.851381445Z",
"tls_cert_hash": null,
"tx_id": "32a4ce2060e4138e6465f907579a9765d50bdb6e89763b064d69664bc4ebf999",
"type": 3,
"version": 0
},
"signature_header": {
"creator": {
"id_bytes": "LS0 ... LQo=",
"mspid": "ECDSARTestNodeMSP"
},
"nonce": "rG24c6sj28YGtCo8PBeQMTJsgPusft6m"
}
}
},
"signature": "MEQCIEIoQw4ZlOB6qc42oQ9L85I4Chs3lKPYgXEbDUEiQUPBAiAf/OQj21xhinlmI6ef7Ufv04KoeIuLwrFlS9lAfltXpw=="
},

]
},
"header": {
"data_hash": "u/d0Jx1D5tEv4WZIpqkjw17J/89klX27L+ukmhNdTQU=",
"number": "384187",
"previous_hash": "DOSnIAu2euoVfpL43+pcQL0aY5DSjMcLrZHpr3kJjfQ="
},
"metadata": {
"metadata": [
"CgI ... cDB",
"",
"AAA=",
"",
"CiCROQhM45JkjcmvLOVSVLqEoS1artoPCQdcipWPGa5/Ww=="
]
}
}

由于数据过大该块内只保留了一个交易,以及去掉了证书部分

以上的数据都可以在BSN测试网服务中查询操作