Fabric源码分析-账本机制01

超级账本的最终目的是将交易记录打包为区块保存到账本中,账本模块用来保存区块,检索区块,记录账本的最终状态。本节介绍了Peer账本的初始化过程。

1. 账本对象

Fabric的orderer会将交易信息打包为Block,Peer会对Block进行校验,然后保存起来,最后修改key的最终状态,在此过程中,还会记录历史信息。因此,对于一个Peer账本来说,需要完成以下功能:

  • 使用Ledger维护整个账本,包括区块的校验,写入,查询
  • 使用LedgerProvider维护账本的通用配置,最终为Channel生成Ledger
  • 使用BlockStore保存区块数据
  • 使用VersionedDBProvider维护最终状态
  • 使用HistoryDBProvider维护历史记录
账本相关对象

1.1 Ledger

common/ledger/ledger_interface.go中定义了账本Ledger的接口及其基本功能,从中我们可以对Ledger的作用有个概要了解。在Fabric中,一个Channel对应着一个账本,一个账本是包含一条由交易记录组成的区块链,以及交易导致的最终状态数据库和历史数据库。下面是Ledger的主要方法:

  • GetBlockchainInfo() 获取当前账本的区块链信息,主要是区块链的高度,当前区块的Hash值及上一个区块的Hash值
  • GetBlockByNumber(blockNumber) 返回指定编号的区块
  • GetBlocksIterator(startBlockNumber) 获取一个从指定编号开始的迭代器,用于不断获取后续的区块
  • Close() 关闭账本
  • Commit(block) 提交区块

可以看出,Ledger主要用于提交区块,查询区块及区块链信息。其他对象中,ResultsIterator用于迭代器,可以不断查询下一个区块,QueryResult是查询结果,PrunePolicy是账本的修剪策略。

1.2 PeerLedger

PeerLedger在core/ledger/ledger_interface.go中定义,实现了1.1 Ledger中的方法,并且添加了额外的其他方法。在common/ledger中,主要是定义通用的Ledger,在core//ledger中是实现。
PeerLedger类额外实现的功能主要是通过Block中的交易txid/hash值获取区块,初次之外,提供了TxSimulator交易模拟器,QueryExecutor查询器,HistoryQueryExecutor历史查询器等功能。

1.3 PeerLedgerProvider

PeerLedgerProvider 保存了账本的通用信息,用来创建/打开账本。例如,Provider里有保存账本的BlockStoreProvider,状态数据库和历史数据库的Provider,对于新创建的通道Channel1,PeerLedgerProvider为其返回一个PeerLedger实例,用来操作Channel1的账本。从下面的主要方法中就可以看出其功能:

  • Create(genesisBlock) 使用创世块创建一个PeerLedger,创世块中包含了账本的相关配置信息
  • Open(ledgerID) 根据账本ID返回一个PeerLedger,ledgerID就是Channel的名称
  • Exists(ledgerID) 判断账本ID是否存在
  • List() 列出当前的所有账本ID

PeerLedgerProvider中包含几个属性:

  • 保存provider信息的数据库,使用leveldb保存provider的内容,在ledgerProvider文件夹中。
  • blockStoreProvider 会生产BlockStore,定义如何保存区块,目前只有一种实现FSBlockSotre,其将区块数据保存到文件系统中。
  • VersionedDBProvider 提供VersionDB的处理类,用于保存账本key值的最终状态。
  • HistoryDBProvider 提供HistoryDB的处理类,用于保存key的历史记录。

2.账本初始化

使用peer start启动peer节点时,首先会初始化与账本相关的对象,主要代码在ledger_mgmt.go中,主要就是创建一个PeerLedgerProvider。
kvLedger是PeerLedger的实现类,由其创建PeerLedgerProvider,主要逻辑为:

  1. 获取peer.fileSystemPath配置的路径,在内部的ledgersData/ledgerProvider文件夹初始化一个leveldb的数据库,保存到idStore中。idStore用于维护账本id相关信息。
  2. 在保存区块的过程中,为了快速检索区块,需要建立索引,因此,创建了一个indexConfig,指定了需要建立索引的字段数组
  3. 生成blockStoreProvider,指定存储区块的路径,每个区块文件的最大大小,索引配置信息
  4. 初始化VersionedDBProvider,根据配置返回leveldb和couchdb的provider
  5. 初始化HistoryDBProvider
  6. 构造PeerLedgerProvider,准备返回
  7. 最后一步,查看是否存在创建账本时发生崩溃的账本id,如果存在的话,需要恢复,在Create()创建账本时会设置这个flag,内容为账本id,成功创建后会删除这个flag。因此如果中途程序崩溃,会留下这个flag。恢复的逻辑是重新创建KVLedger,使用blockStore从文件中获取blockchaininfo,如果高度为0,说明还没有提交创世块,删除flag即可,如果高度为1,已经提交创世块,使用idStore维护账本id,其他情况说明账本已经创建了。

3. idStore

idStore定义在kv_ledger_provider.go中,内部包含一个用于操作leveldb的成员db,初始化账本的时候,会为provider创建一个leveldb数据库,主要功能:

  1. underConstructionFlag 操作underConstructionLedgerKey,这个key用来记录账本创建过程中是否发生了异常,发生异常时会有补偿机制重建账本。
  2. 在PeerLedgerProvider使用Create(genesisBlock *common.Block)创建PeerLedger时,会将账本的id和创世块内容组成键值对保存在数据库中。
  3. 获取所有的账本id,遍历数据库的key,如果是账本id的前缀,则说明有这个账本,存放在返回的list中。

4.BlockStore

BlockStore定义在common/ledger/blkstorage/blockstorage.go中,只有一个实现fsblkstorage,在文件系统中保存区块。这是账本机制的重要组成部分,PeerLedger的实现kvLedger中的大部分操作都是使用的BlockStore,VersionedDB和HistroyDB。

下面是BlockStore接口的方法,可以看到,主要是添加取款,获取区块链信息,检索区块的功能。

type BlockStore interface {
    AddBlock(block *common.Block) error
    GetBlockchainInfo() (*common.BlockchainInfo, error)
    RetrieveBlocks(startNum uint64) (ledger.ResultsIterator, error)
    ......
    Shutdown()
}

fs_blockstore.go中的fsBlockStore实现了BlockStore,在文件系统中保存区块。需要注意的是fsBlockStore中有一个blockfileMgr,里面负责具体的文件操作。

4.1 blockfileMgr

blockfileMgr定义在blockfile_mgr.go中,用来管理将Block写入文件,建立索引,获取Block。blockfileMgr中有如下几个重要的组件,用来帮助完成存储Block的操作。
blockfileWriter
blockfileWriter用于将数据写入文件,由于fsBlockStore将区块保存到了多个文件中,blockfileWriter记录了写入文件的文件夹和操作当前文件的os.File

checkpointinfo
checkpointInfo定义在blockfile_mgr.go中,用来记录当前最后一个文件的序号,已写入文件的字节数,最后一个Block的编号信息。checkpointinfo会被保存在数据库中,每次初始化时,可以从数据库中读出,更新Block时,需要更新checkpointinfo并更新数据库。

BlockchainInfo
BlockchainInfo记录了区块链的状态,如高度,最后一个Block的Hash等

blockIndex

blockIndex用来建立索引,indexBlock

4.2 提交Block

blockfileMgr的addBlock负责添加Block,主要逻辑为:

  1. 校验区块编号,必须与BlockChain的高度相同,例如:区块链现有3个区块,编号分别为0-1-2,高度为3,那么新添加的Block编号需要是3。
  2. 使用protobuf将Block序列化,Block的结构为
common.Block
  - BlockHeader
      - Number  > 区块编号
      - PrevHash > 上一个区块的Hash
      - DataHash  > BlockData的Hash
  - BlockData 
      - txEnvelope  > 一个交易
        - Payload 
          - Header  > 头
            - ChannelHeader  > 类型 版本和ChannelID
            - SignatureHeader > 签名头
        - Signature  
          - Creator  > 创建者
          - Nonce   >  随机数
  - BlockMetadata

在序列化的过程中,可以提取出Block的交易id列表,元数据等信息,保存在serializedBlockInfo中;还可以计算出区块占用的字节数。

  1. 计算Block的Hash值,逻辑为将Block的编号,上一个Block的Hash和当前区块的DataHash使用ASN.1 编码为byte[],然后再使用SHA-256生成当前区块的Hash值。
  2. 将Block的byte数组(记为A)写入文件,写入内容为B:A的长度+A的内容,那么B的大小就是要写入文件的总大小,由于每个文件有大小限制,如果剩余的文件容量不足以支持写入,就重新创建一个文件并写入。blockfileMgr中维护了checkpointinfo,内部记录了账本当前所处于的文件及offset,每次写入之后,都会修改checkpointinfo信息。
  3. 在写入数据期间,如果发生error,会将文件恢复到写入之前的状态,并抛出异常。
  4. 更新checkpointInfo的信息,并保存到数据库中。
  5. 写入成功后,使用blockIndex,创建索引,这样可以根据block编号,hash值,交易id快速定位到Block所在文件的位置
  6. 添加Block后,BlockChainInfo最后一个Block发生变化,更新这些信息,。

你可能感兴趣的:(Fabric源码分析-账本机制01)