Fabric 账本数据块结构解析:如何解析账本中的智能合约交易数据

Posted BSN研习社

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Fabric 账本数据块结构解析:如何解析账本中的智能合约交易数据相关的知识,希望对你有一定的参考价值。

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智能合约,合约提供了两个合约方法SetQuery

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.BlockReqDataBodyTxId: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.goBlock经过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一致。
Payloadpeer.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为合约的返回信息,包含StatusMessagePayload
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测试网服务中查询操作

Hyperledger Fabric 账本结构解析

前言

  现在很多人都在从事区块链方面的研究,作者也一直在基于Hyperledger Fabric做一些开发工作。为了方便后来人更快的入门,本着“开源”的精神,在本文中向大家讲解一下Hyperledger Fabric账本的结构和原理。作者解析的Fabric的工程版本为v1.0.1,在新版本中可能会有些许偏差。

  ps:作者默认各位读者已经具备了一定的区块链基本知识,不再做一些基础知识的阐述。

Hyperledger Fabric账本的结构

  在作者最初了解bitcoin的时候有一个疑问:矿工如何校验一笔交易中引入的Utxo是否合法呢?如果过是从创世块开始遍历工作量会随着块的增加而逐渐变大。当作者研究过Bitcoin的源码后发现,在Bitcoin中有一部分模块是用来存储当前所有Utxo的,它采用的是levelDB数据库,在校验一笔交易中的Utxo是否合法时直接去DB中检索即可。

  Bitcoin的这种做法给作者总结为如下:“单纯”的区块链账本存储区块数据即可,而为了方便支持各种功能往往会提取出一些数据(重要的、频繁访问的)独立存储。在Bitcoin的世界中矿工的共识实际上是对当前Utxo的共识,所以它将Utxo从区块量账本中提取出来独立的存储,那在Hyperledger Fabric中需要从区块账本中提取出的数据是什么?

  Fabric中是一个针对商业应用的分布式账本技术,本身并不存在代币,你可以在它的基础上进行二次开发,利用Fabric的智能合约实现自己需要的各种业务--包括发放代币。在它的智能合约模块中有两个最关键的接口中:shim.PutState(Key, Value) 和 shim.GetState(Key),这个是用来向节点本地读写数据的指令,它呈现给智能合约的是一个key-value结构的非关系形数据库。然后Client通过调用智能合约执行业务逻辑,智能合约来进行数据的读写操作的(在区块链的世界中调用智能合约的Request统称为Transaction,这是约定俗称的,虽然在Fabric中没有代币),这和传统的中心化Web服务十分相似,只不过Tomcat换成了Fabric,而后台的jar包换成了智能合约,中心化的APP变成了分布式的DAPP。

  现在参照作者从Bitcoin中总结的规律,在Fabric智能合约中读写的业务数据符合重要的、频繁访问的特征,应该独立存储,这个数据库的名称为StateDB 。除了StateDB以外我们还要保存区块数据,在Fabric里面它有一个自己的FileSystem,用来存储区块数据,这个文件系统是存储在本地的文件中的。区块数据被连续的写入到本地的BlockFile中,通过File.io来读写数据,Fabric工程是用go语言编写的,工程中操作文件IO的包是os ,包中常用操作文件的函数如下:    

type File

    func Create(name string) (*File, error)
    func NewFile(fd uintptr, name string) *File
    func Open(name string) (*File, error)
    func OpenFile(name string, flag int, perm FileMode) (*File, error)
    func Pipe() (r *File, w *File, err error)
    func (f *File) Chdir() error
    func (f *File) Chmod(mode FileMode) error
    func (f *File) Chown(uid, gid int) error
    func (f *File) Close() error
    func (f *File) Fd() uintptr
    func (f *File) Name() string
    func (f *File) Read(b []byte) (n int, err error)
    func (f *File) ReadAt(b []byte, off int64) (n int, err error)
    func (f *File) Readdir(n int) ([]FileInfo, error)
    func (f *File) Readdirnames(n int) (names []string, err error)
    func (f *File) Seek(offset int64, whence int) (ret int64, err error)
    func (f *File) SetDeadline(t time.Time) error
    func (f *File) SetReadDeadline(t time.Time) error
    func (f *File) SetWriteDeadline(t time.Time) error
    func (f *File) Stat() (FileInfo, error)
    func (f *File) Sync() error
    func (f *File) Truncate(size int64) error
    func (f *File) Write(b []byte) (n int, err error)
    func (f *File) WriteAt(b []byte, off int64) (n int, err error)
    func (f *File) WriteString(s string) (n int, err error)

type FileInfo

    func Lstat(name string) (FileInfo, error)
    func Stat(name string) (FileInfo, error)

type FileMode

    func (m FileMode) IsDir() bool
    func (m FileMode) IsRegular() bool
    func (m FileMode) Perm() FileMode
    func (m FileMode) String() string

 

Q1:以上所有的接口都不支持复杂的查询功能,读者们可以想象一下,当要去BlockFile中读取一个块的时候要什么样的条件才能快速的找到对应块?

A1: 如果查询者能知道一个Block数据在文件中的便宜量则可以快速定位到Block,相反则只能通过遍历的方式。为了解决这个问题Fabric 将Block在BlockFile文件中的偏移量记载到了一个非关系形DB中,这个DB的名称为indexDB

Q2:区块数据是不可篡改的,也就是说BlockFile的size是持续增长的,如果size过大超过了位置偏移量的最大范围怎么办?

A2: 将区块数据存储在多个文件块中,以blockfile_ 为前缀,以块的生成次序为后缀。

  解决了上面的两个问题后,查询一个块数据的过程如下:

  setup1: 从indexDB中读取Block的位置信息(blockfile的编号、位置偏移量);

  setup2: 打开对应的blockfile,位移到指定位置,读取Block数据。

  Fabric账本的整体结构图来如下图所示:

  其中ledgersData是整个账本的根目录,作者会逐个文件夹进行解析:

  (1)chains:chains/chains下包含的mychannel是对应的channel的名称,因为Fabric是有多channel的机制,而channel之间的账本是隔离的,每个channel都有自己的账本空间。chains/index下面包含的是levelDB数据库文件,在Fabric中默认所有的数据库都是LevelDB,这个原因作者下面会讲到,DB中存储的就是我们上面说的区块索引部分。chains/chains和chains/index就是上面所说的File System和indexDB;

  (2)stateLeveldb: 同样是levelDB数据库,存储的就是我们上面所说的智能合约putstate写入的数据;

  (3)ledgerProvider:数据库内存储的是当前节点所包含channel的信息(已经创建的channel id 和正在创建中的channel id),主要是为了Fabric的多channel机制服务的;

    (4)historyLeveldb:数据库内存储的是智能合约中写入的key的历史记录的索引地址。

Hyperledger Fabric账本详解

  这一部分作者会结合几个问题对Hyperledger Fabric 区块链账本的内在原理进行深入的剖析,其中会涉及到源码部分的解析,如果读者只是想对账本机制做一些简单了解,可以跳过源码分析的部分。

  Q3:  智能合约读写数据到stateLeveldb的流程?

  A3:作者以GetState()接口为例进行说明:

  首先,读者们需要明确Hyperledger Fabric的智能合约是在docker容器中运行的,而stateLeveldb是位于节点服务器本地的。同时,Fabric没有将stateLevelDB映射到docker容器中,所以智能合约不能直接访问stateLevelDB。

  Fabric中是通过rpc服务调用的方式,来解决这个问题的。开发过Fabric智能合约的读者应该清楚,Fabric提供了一个中间层Shim包,同时在Peer节点中默认会开启一个grpc服务 ChainCodeService。智能合约文件在编译的时候会自动引入shim包,而shim包中在GetState的过程中会向节点的ChainCodeService发送请求到节点,然后节点再从本地的stateLevelDB中读取请求的数据,返回给智能合约。

  流程图:

  以上只是读数据的过程,写数据远比读数据更为复杂。

  //TODO 写数据过程

  Q4:fabric支持交易的查询接口,那么交易查询是如何实现的?

  A4: 交易查询与Block查询的实现原理一致,节点在写入账本到本地的时候,会将交易在FileSystem中的索引也写入indexDB中。

  Q5: indexDB是levelDB,它是如何实现复杂查询的,比如交易的范围查询?

  未完待续... ...

以上是关于Fabric 账本数据块结构解析:如何解析账本中的智能合约交易数据的主要内容,如果未能解决你的问题,请参考以下文章

Hyperledger Fabric 账本结构解析

如何计算超级账本结构中的默克尔根?

超级账本Fabric的块和交易大小

HyperLeger Fabric开发——HyperLeger Fabric账本存储

超级账本Fabric 层次结构以及核心模块的介绍

区块链100例 编写脚本快速搭建超级账本 Fabric