Btcd区块链的构建(一)

我们在介绍Btcd协议消息时提到,协议设计的目标就是同步transaction或者block,最终在各节点上形成一致的区块链。本文开始,我们将逐步分析节点在收到transaction或者block后如何处理它们,如如何将transaction打包成区块并挖矿、如何将block添加到区块链中等问题。同步transaction后,“矿工”可以将交易打包进新的区块,并向网络传播新区块。所以,对于独立“挖矿”的节点或者通过“矿池”与“矿工”连接的“全节点”而言,它们有两方面的区块来源,即“挖矿”产生的新区块和收到的其它节点转发的区块,如下图所示。

同步其它节点转发的区块,就是通过协议消息inv-getdata-block实现的,我们在《Btcd区块链协议消息解析》中介绍过这一过程;“挖矿”的过程我们将在后文中详细介绍。无论是“挖”出新区块还是收到其他节点转发的区块,都需要经过一系列验证后,再加入到区块链上;同时,当区块链的共识机制需要作微调,即发生“软分叉”时,节点之间也需要一个共识机制来支持新的区块或者兼容老的区块,这些过程或者机制的实现位于btcd/blockchain包中。我们将重点介绍btcd/blockchain包,其中的文件有:

  • fullblocktests文件夹: 包含自动生成测试用例的实现类,用于测试区块的共识参数;
  • indexers文件夹: 实现了transaction到block之间和transaction到关联的bitcoin地址之间的索引关系;
  • testdata文件夹: 包含用于测试的block数据;
  • process.go: ProcessBlock()方法所在的文件,它是btcd/blockchain对区块进行处理的入口方法,也是我们重点分析对象;
  • accept.go: maybeAcceptBlock()方法所在的文件,它是区块被链接到区块链上的入口方法;
  • validate.go: 实现了验证transaction和block的各种方法,如CheckBlockSanity()、CheckProofOfWork、CheckTransactionSanity等等;
  • chain.go: 定义了BlockChain类型,包括了区块加入区块链,或者区块链构建的主要实现过程;
  • blockindex.go: 实现了blockIndex,用于维护blockchain上block与其hash之间的映射关系和父block与子block(s)的关系;
  • chainio.go: 提供了向db中读写区块或者相关元数据(如ThresholdCaches、BlockIndex等等)的方法;
  • checkpoints.go: 实现了BlockChain中与Checkpoint相关的方法,如findPreviousCheckpoint()等等,BlockChain中的checkpoints是一系列指定的区块,内置在bitcoin节点中,用于验证区块工作量;
  • compress.go: 实现了一种改进的VLQ(Variable Length Quantity)正整数编码方式,同时实现了对交易输出额进行压缩编码和输出脚本(锁定脚本)进行压缩编码的算法,进而实现了对交易输出TxOut的压缩;
  • difficulty.go: 提供了与区块难度值相关的方法,如区块头里的难度值与整数值之间的转换、通过难度值计算工作量和难度值调整算法等等;
  • mediatime.go: 定义了MedianTimeSource接口和其实现mediaTime类型,通过计算各时间源的中位值来较正当前时间;
  • merkle.go: 实现了构建Merkle树的算法;
  • notifications.go: 定义了NotificationCallback和Notification,用于blockchain向其消费者回调NTBlockAccepted、NTBlockConnected及NTBlockDisconnected事件;
  • scriptval.go: 实现了验证交易中的锁定脚本与解锁脚本的机制,支持并发地验证多个脚本对;
  • thresholdstate.go: 定义了新的共识规则在向区块链网络部署时的状态,按部署或者节点接受的进度依次为Defined、Started、LockedIn、Active或者Failed等状态,更重要地,实现了状态转换的算法和计算区块上某个共识规则的部署状态的方法,我们将在后文中详细介绍;
  • timersort.go: 实现了支持升序排序的时间戳集合类型timeSorter,timeSorter实际上是[]int64类型,这是Go语言特性的一个体现,即通过类型定义可以扩展任意类型,包括基础类型的方法集;
  • versionbits.go: 实现了BIP9中描述的关于区块版本号的定义,区块版本号是一个32位且按小端模式存储的整型值,以001开头,其后的每一位代表一个BIP部署;同时也实现了根据部署状态确定下一个区块的目标版本号的方法,用于设定新“挖”出区块的版本号,我们将在后文中详细分析;
  • utxoviewpoint.go: 维护了UTXO的集合,包括有新的transaction时向集合中添加新的UTXO或者移除已经花费的UTXO;
  • doc.go: 包blockchain的doc说明;
  • error.go: 定义了与构建区块链相关的error类型及对应的字符串;
  • log.go: 提供了logger相关的方法;
  • xxx_test.go: 各文件对应的测试文件;

ProcessBlock()是btcd/blockchain中开始对block进行各种验证并接入区块链的核心方法,从上图中也可以看到,收到其它节点同步过来的区块或者矿工“挖”出块后均交由ProcessBlock处理。接下来,我们将从ProcessBlock()入手逐步分析区块处理的各个步骤,为了便于后续分析,我们看介绍BlockChain的定义:

//btcd/blockchain/chain.go

// BlockChain provides functions for working with the bitcoin block chain.
// It includes functionality such as rejecting duplicate blocks, ensuring blocks
// follow all rules, orphan handling, checkpoint handling, and best chain
// selection with reorganization.
type BlockChain struct {
    // The following fields are set when the instance is created and can't
    // be changed afterwards, so there is no need to protect them with a
    // separate mutex.
    checkpoints         []chaincfg.Checkpoint
    checkpointsByHeight map[int32]*chaincfg.Checkpoint
    db                  database.DB
    chainParams         *chaincfg.Params
    timeSource          MedianTimeSource
    notifications       NotificationCallback
    sigCache            *txscript.SigCache
    indexManager        IndexManager

    // The following fields are calculated based upon the provided chain
    // parameters.  They are also set when the instance is created and
    // can't be changed afterwards, so there is no need to protect them with
    // a separate mutex.
    //
    // minMemoryNodes is the minimum number of consecutive nodes needed
    // in memory in order to perform all necessary validation.  It is used
    // to determine when it's safe to prune nodes from memory without
    // causing constant dynamic reloading.  This is typically the same value
    // as blocksPerRetarget, but it is separated here for tweakability and
    // testability.
    minRetargetTimespan int64 // target timespan / adjustment factor
    maxRetargetTimespan int64 // target timespan * adjustment factor
    blocksPerRetarget   int32 // target timespan / target time per block
    minMemoryNodes      int32

    // chainLock protects concurrent access to the vast majority of the
    // fields in this struct below this point.
    chainLock sync.RWMutex

    // These fields are configuration parameters that can be toggled at
    // runtime.  They are protected by the chain lock.
    noVerify bool

    // These fields are related to the memory block index.  The best node
    // is protected by the chain lock and the index has its own locks.
    bestNode *blockNode
    index    *blockIndex

    // These fields are related to handling of orphan blocks.  They are
    // protected by a combination of the chain lock and the orphan lock.
    orphanLock   sync.RWMutex
    orphans      map[chainhash.Hash]*orphanBlock
    prevOrphans  map[chainhash.Hash][]*orphanBlock
    oldestOrphan *orphanBlock

    // These fields are related to checkpoint handling.  They are protected
    // by the chain lock.
    nextCheckpoint  *chaincfg.Checkpoint
    checkpointBlock *btcutil.Block

    // The state is used as a fairly efficient way to cache information
    // about the current best chain state that is returned to callers when
    // requested.  It operates on the principle of MVCC such that any time a
    // new block becomes the best block, the state pointer is replaced with
    // a new struct and the old state is left untouched.  In this way,
    // multiple callers can be pointing to different best chain states.
    // This is acceptable for most callers because the state is only being
    // queried at a specific point in time.
    //
    // In addition, some of the fields are stored in the database so the
    // chain state can be quickly reconstructed on load.
    stateLock     sync.RWMutex
    stateSnapshot *BestState

    // The following caches are used to efficiently keep track of the
    // current deployment threshold state of each rule change deployment.
    //
    // This information is stored in the database so it can be quickly
    // reconstructed on load.
    //
    // warningCaches caches the current deployment threshold state for blocks
    // in each of the **possible** deployments.  This is used in order to
    // detect when new unrecognized rule changes are being voted on and/or
    // have been activated such as will be the case when older versions of
    // the software are being used
    //
    // deploymentCaches caches the current deployment threshold state for
    // blocks in each of the actively defined deployments.
    warningCaches    []thresholdStateCache
    deploymentCaches []thresholdStateCache

    // The following fields are used to determine if certain warnings have
    // already been shown.
    //
    // unknownRulesWarned refers to warnings due to unknown rules being
    // activated.
    //
    // unknownVersionsWarned refers to warnings due to unknown versions
    // being mined.
    unknownRulesWarned    bool
    unknownVersionsWarned bool
}

其中各个字段的意义如下:

  • checkpoints: 链上预设的一些checkpoints,节点启动时指定,实际由链上的某些高度上的区块充当;
  • checkpointsByHeight: 记录了checkpoints和对应的区块的高度之间的映射关系,初始化时由checkpoints解析而来;
  • db: 存储区块的database对象;
  • chainParams: 与区块链相关的参数配置,如网络号、创世区块(Genesis Block)、难度调整周期、DNSSeeds等等;对这些参数进行定制即可以生成一个与Bitcoin类似的新的"代币";
  • timeSource: 用于较正节点时钟的“时钟源”,节点可以在与Peer进行version消息交换的时候把Peer添加到自己的“时钟源”中,这样节点通过计算与不同Peer节点的时钟差的中位值,来估计其与网络同步时间的差,从而在利用timestamp进行区块较验之前先较正自己的时钟,以防节点时钟未同步造成较验失败;
  • notifications: 向bockchain注册的回调事件接收者;
  • sigCache: 用于缓存公钥与签名验证的结果,对于已经通过解锁脚本和锁定脚本验证的交易,对应的公钥和签名会被缓存,后续相同的交易验证时可以直接从缓存中读到验证结果;
  • indexManager: 用于索引链上transaction或者block的indexer;
  • minRetargetTimespan: 难度调整的最小周期,公链上其值为3.5天;
  • maxRetargetTimespan: 难度调整的最大周期,公链上其值为56天;
  • blocksPerRetarget: 难度调整周期内的区块数,公链上难度调整周期是14天,对应的区块数是2016个;
  • minMemoryNodes: 该值暂未用到;
  • chainLock: 保护访问区块链的读写锁;
  • noVerify: 是否关闭脚本验证的开关;
  • bestNode: 指向主链上的“尾”节点,即高度最高的区块;
  • index: 指向一个区块索引器,用于索引实例化后内存中的各区块;
  • orphanLock: 保护“孤儿区块”相关对象的读写锁;
  • orphans: 记录“孤儿区块”与其Hash之间的映射;
  • prevOrphans: 记录“孤儿区块”与其父区块Hash之间的映射,当父区块写入区块链后,要检索prevOrphans将对应的区块从“孤儿区块池”从移除并写入区块链;
  • oldestOrphan: 处于“孤儿”状态时间最长的区块,“孤儿区块池”最多维护100个“孤儿区块”,当超过限制时,开始移除“最老”的“孤儿区块”;
  • stateLock: 保护stateSnapshot的读写锁;
  • stateSnapshot: 主链相关信息的快照;
  • warningCaches: 缓存区块对于所有可能的部署的thresholdState,用于当节点收到大量新的版本的区块,且对应的共识规则在新的区块里已经部署或者即将部署时,发出警告提示,为了兼容新版本的区块,可能需要升级btcd版本;
  • deploymentCaches: 缓存区块对于已知的部署提案的thresholdState;
  • unknownRulesWarned: 标识是否已经警告过未知共识规则已经部署或者将被部署;
  • unknownVersionsWarned: 标识是否已经警告过收到过多未知新版本的区块;当有新版本的区块时,往往有新的共识规则正在部署,所以警告未知新版本与未知共识规则部署是相关的;

在了解了BlockChain的定义后,我们开始从ProcessBlock()分析处理区块的各个环节:

//btcd/blockchain/process.go

// ProcessBlock is the main workhorse for handling insertion of new blocks into
// the block chain.  It includes functionality such as rejecting duplicate
// blocks, ensuring blocks follow all rules, orphan handling, and insertion into
// the block chain along with best chain selection and reorganization.
//
// When no errors occurred during processing, the first return value indicates
// whether or not the block is on the main chain and the second indicates
// whether or not the block is an orphan.
//
// This function is safe for concurrent access.
func (b *BlockChain) ProcessBlock(block *btcutil.Block, flags BehaviorFlags) (bool, bool, error) {
    b.chainLock.Lock()
    defer b.chainLock.Unlock()

    fastAdd := flags&BFFastAdd == BFFastAdd
    dryRun := flags&BFDryRun == BFDryRun

    blockHash := block.Hash()
    log.Tracef("Processing block %v", blockHash)

    // The block must not already exist in the main chain or side chains.
    exists, err := b.blockExists(blockHash)                                              (1)
    if err != nil {
        return false, false, err
    }
    if exists {
        str := fmt.Sprintf("already have block %v", blockHash)
        return false, false, ruleError(ErrDuplicateBlock, str)
    }

    // The block must not already exist as an orphan.
    if _, exists := b.orphans[*blockHash]; exists {                                      (2)
        str := fmt.Sprintf("already have block (orphan) %v", blockHash)
        return false, false, ruleError(ErrDuplicateBlock, str)
    }

    // Perform preliminary sanity checks on the block and its transactions.
    err = checkBlockSanity(block, b.chainParams.PowLimit, b.timeSource, flags)           (3)
    if err != nil {
        return false, false, err
    }

    // Find the previous checkpoint and perform some additional checks based
    // on the checkpoint.  This provides a few nice properties such as
    // preventing old side chain blocks before the last checkpoint,
    // rejecting easy to mine, but otherwise bogus, blocks that could be
    // used to eat memory, and ensuring expected (versus claimed) proof of
    // work requirements since the previous checkpoint are met.
    blockHeader := &block.MsgBlock().Header
    checkpointBlock, err := b.findPreviousCheckpoint()
    if err != nil {
        return false, false, err
    }
    if checkpointBlock != nil {
        // Ensure the block timestamp is after the checkpoint timestamp.
        checkpointHeader := &checkpointBlock.MsgBlock().Header
        checkpointTime := checkpointHeader.Timestamp
        if blockHeader.Timestamp.Before(checkpointTime) {                                (4)
            str := fmt.Sprintf("block %v has timestamp %v before "+
                "last checkpoint timestamp %v", blockHash,
                blockHeader.Timestamp, checkpointTime)
            return false, false, ruleError(ErrCheckpointTimeTooOld, str)
        }
        if !fastAdd {
            // Even though the checks prior to now have already ensured the
            // proof of work exceeds the claimed amount, the claimed amount
            // is a field in the block header which could be forged.  This
            // check ensures the proof of work is at least the minimum
            // expected based on elapsed time since the last checkpoint and
            // maximum adjustment allowed by the retarget rules.
            duration := blockHeader.Timestamp.Sub(checkpointTime)
            requiredTarget := CompactToBig(b.calcEasiestDifficulty(
                checkpointHeader.Bits, duration))
            currentTarget := CompactToBig(blockHeader.Bits)
            if currentTarget.Cmp(requiredTarget) > 0 {                                   (5)
                str := fmt.Sprintf("block target difficulty of %064x "+
                    "is too low when compared to the previous "+
                    "checkpoint", currentTarget)
                return false, false, ruleError(ErrDifficultyTooLow, str)
            }
        }
    }

    // Handle orphan blocks.
    prevHash := &blockHeader.PrevBlock
    prevHashExists, err := b.blockExists(prevHash)
    if err != nil {
        return false, false, err
    }
    if !prevHashExists {
        if !dryRun {
            log.Infof("Adding orphan block %v with parent %v",
                blockHash, prevHash)
            b.addOrphanBlock(block)                                                      (6)
        }

        return false, true, nil
    }

    // The block has passed all context independent checks and appears sane
    // enough to potentially accept it into the block chain.
    isMainChain, err := b.maybeAcceptBlock(block, flags)                                 (7)
    if err != nil {
        return false, false, err
    }

    // Don't process any orphans or log when the dry run flag is set.
    if !dryRun {
        // Accept any orphan blocks that depend on this block (they are
        // no longer orphans) and repeat for those accepted blocks until
        // there are no more.
        err := b.processOrphans(blockHash, flags)                                        (8)
        if err != nil {
            return false, false, err
        }

        log.Debugf("Accepted block %v", blockHash)
    }

    return isMainChain, false, nil
}

ProcessBlock()输入的是指向btcutil.Block类型的block,它对wire.MsgBlock进行了封装,可以看作是访问wire.MsgBlock的辅助类型,在btcd/blockchain中看到的block类型均是btcutil.Block类型,所以在解析代码之前,我们先看一下它的定义:

//btcd/vendor/github.com/btcsuite/btcutil/block.go

// Block defines a bitcoin block that provides easier and more efficient
// manipulation of raw blocks.  It also memoizes hashes for the block and its
// transactions on their first access so subsequent accesses don't have to
// repeat the relatively expensive hashing operations.
type Block struct {
    msgBlock        *wire.MsgBlock  // Underlying MsgBlock
    serializedBlock []byte          // Serialized bytes for the block
    blockHash       *chainhash.Hash // Cached block hash
    blockHeight     int32           // Height in the main block chain
    transactions    []*Tx           // Transactions
    txnsGenerated   bool            // ALL wrapped transactions generated
}

ProcessBlock()输出的第一个值表示区块是否加入了主链,第二值表示区块是否是“孤儿区块”。其主要执行步骤是:

  1. 首先检查区块是否已经在链上,如代码(1)处所示;
  2. 然后检查区块是否在“孤儿区块池”中,如代码(2)处所示;
  3. 代码(3)处调用checkBlockSanity()对区块结构进行检查,包括对区块头,如工作量和Merkle树根等等,和交易集合的检查;
  4. 通过区块结构检查后,根据最近的Checkpoint与区块之间的时间差,计算预期的最小工作量,如果区块的工作量低于预期的最小工作量则被拒绝,如代码(5)所示; 这是通过Checkpoint机制防止伪造工作量证明的过程,需要注意的是,区块头中表示目标难度的值越大,则表示工作量越小,反之,其值越小,则表示工作量越大;
  5. 通过Checkpoint检查后,接着检测区块的父区块是否在链上,如果不在链上,则将区块加入“孤儿区块池”,如代码(6)处所示;
  6. 如果父区块在链上,代码(7)处调用maybeAcceptBlock()对区块先进行上下文检查,如根据父区块计算期望工作量、期望的timestamp范围、区块高度是否正确等等,然后根据父区块是否在主链上决定是扩展主链还是侧链,并进一步对区块中的交易进行验证;交易验证通过后,最终将区块接入区块链,如果扩展的是侧链,还要比较侧链扩展后的工作量是否超过主链,如果超过,则将侧链变为主链,其中详细的过程将在后文中介绍;
  7. 区块加入区块链后,最后调用processOrphans()检测“孤儿区块池”中是否有“孤儿区块”的父区块是刚刚入链的区块,如果有,则将“孤儿区块”也加入区块链,并重复这一检查;

通过ProcessBlock()我们可以看到区块处理的几个阶段:

其中的checkBlockSanity、maybeAcceptBlock及processOrphans等过程中又有很复杂的步骤,它们到底作了哪些检查,如何保证区块链的一致性,我们将在下一篇文章《Btcd区块链的构建(二)》中展开介绍。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • 在《Btcd区块链的构建(三)》[https://www.jianshu.com/p/06fe9de09172]和...
    oceanken阅读 1,198评论 13 7
  • 上一篇文章我们介绍了maybeAcceptBlock()中将区块连入主链的主要步骤,其中checkConnectB...
    oceanken阅读 1,674评论 13 4
  • 上一篇[https://www.jianshu.com/p/5ddbdb6a9843]文章中,我们分析了check...
    oceanken阅读 1,163评论 0 4
  • 上一篇文章我们分析了ProcessBlock()的主要步骤,其中的checkBlockSanity()、maybe...
    oceanken阅读 1,327评论 0 5
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 120,482评论 2 7