Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

检查点(CheckPoints)调研报告 #343

Closed
AlexiaChen opened this issue Dec 18, 2019 · 3 comments
Closed

检查点(CheckPoints)调研报告 #343

AlexiaChen opened this issue Dec 18, 2019 · 3 comments
Labels
documentation Improvements or additions to documentation

Comments

@AlexiaChen
Copy link
Contributor

BTC 检查点(CheckPoint)调研报告

BTC源码

BTC官方源码是C++写的,但是有一个官方认可的全节点是golang写的,叫btcd。

此调研报告是基于btcd源码的调研。毕竟golang的源码可读性高太多了,C++短期一下子可能搞不明白。

BTCD检查点的实现

检查点的组织结构

检查点的实现源码文件叫checkpoints.go,在blockchain目录下。

CheckPoint被描述为一个块高度和块Hash的二元组结构体:

type Checkpoint struct {
	Height int32
	Hash   *chainhash.Hash
}

该结构体对象出现在BlockChain结构体中,为该类的一个属性,从目录结构和代码组织结构上看,CheckPoint就是主链的固有属性。

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 //配置初始化的时候,构造map,key是高度,value是CheckPoint结构体,按高度查找检查点
    
    // there are more field of this struct
    ...
}

注意看checkpoint的类型,是在chaincfg模块里面的,跳转过去是params.go里面实现的,配置的参数大部分硬编码在这文件里:

硬编码在Params的结构体里:

type Params struct {
    // there are more field of this struct
    ...

    // Checkpoints ordered from oldest to newest. 
	Checkpoints []Checkpoint
    // there are more field of this struct
    ...

}

从上面可以看到,是个CheckPoint的结构体数组,已排好序的,按照最老的到最新的区块排序。

这个配置参数结构体的值,是被硬编码设置的,主网,测试网都分别硬编码了属于自己的配置,下面只看主网的配置,重点看CheckPoint的配置:

// MainNetParams defines the network parameters for the main Bitcoin network.
var MainNetParams = Params{
	Name:        "mainnet",
	Net:         wire.MainNet,
	DefaultPort: "8333",
	DNSSeeds: []DNSSeed{
		{"seed.bitcoin.sipa.be", true},
		{"dnsseed.bluematt.me", true},
		{"dnsseed.bitcoin.dashjr.org", false},
		{"seed.bitcoinstats.com", true},
		{"seed.bitnodes.io", false},
		{"seed.bitcoin.jonasschnelli.ch", true},
	},

	// Chain parameters
	GenesisBlock:             &genesisBlock,
	GenesisHash:              &genesisHash,
	PowLimit:                 mainPowLimit,
	PowLimitBits:             0x1d00ffff,
	BIP0034Height:            227931, // 000000000000024b89b42a942fe0d9fea3bb44ab7bd1b19115dd6a759c0808b8
	BIP0065Height:            388381, // 000000000000000004c2b624ed5d7756c508d90fd0da2c7c679febfa6c4735f0
	BIP0066Height:            363725, // 00000000000000000379eaa19dce8c9b722d46ae6a57c2f1a988119488b50931
	CoinbaseMaturity:         100,
	SubsidyReductionInterval: 210000,
	TargetTimespan:           time.Hour * 24 * 14, // 14 days
	TargetTimePerBlock:       time.Minute * 10,    // 10 minutes
	RetargetAdjustmentFactor: 4,                   // 25% less, 400% more
	ReduceMinDifficulty:      false,
	MinDiffReductionTime:     0,
	GenerateSupported:        false,

	// Checkpoints ordered from oldest to newest.
	Checkpoints: []Checkpoint{
		{11111, newHashFromStr("0000000069e244f73d78e8fd29ba2fd2ed618bd6fa2ee92559f542fdb26e7c1d")},
		{33333, newHashFromStr("000000002dd5588a74784eaa7ab0507a18ad16a236e7b1ce69f00d7ddfb5d0a6")},
		{74000, newHashFromStr("0000000000573993a3c9e41ce34471c079dcf5f52a0e824a81e7f953b8661a20")},
		{105000, newHashFromStr("00000000000291ce28027faea320c8d2b054b2e0fe44a773f3eefb151d6bdc97")},
		{134444, newHashFromStr("00000000000005b12ffd4cd315cd34ffd4a594f430ac814c91184a0d42d2b0fe")},
		{168000, newHashFromStr("000000000000099e61ea72015e79632f216fe6cb33d7899acb35b75c8303b763")},
		{193000, newHashFromStr("000000000000059f452a5f7340de6682a977387c17010ff6e6c3bd83ca8b1317")},
		{210000, newHashFromStr("000000000000048b95347e83192f69cf0366076336c639f9b7228e9ba171342e")},
		{216116, newHashFromStr("00000000000001b4f4b433e81ee46494af945cf96014816a4e2370f11b23df4e")},
		{225430, newHashFromStr("00000000000001c108384350f74090433e7fcf79a606b8e797f065b130575932")},
		{250000, newHashFromStr("000000000000003887df1f29024b06fc2200b55f8af8f35453d7be294df2d214")},
		{267300, newHashFromStr("000000000000000a83fbd660e918f218bf37edd92b748ad940483c7c116179ac")},
		{279000, newHashFromStr("0000000000000001ae8c72a0b0c301f67e3afca10e819efa9041e458e9bd7e40")},
		{300255, newHashFromStr("0000000000000000162804527c6e9b9f0563a280525f9d08c12041def0a0f3b2")},
		{319400, newHashFromStr("000000000000000021c6052e9becade189495d1c539aa37c58917305fd15f13b")},
		{343185, newHashFromStr("0000000000000000072b8bf361d01a6ba7d445dd024203fafc78768ed4368554")},
		{352940, newHashFromStr("000000000000000010755df42dba556bb72be6a32f3ce0b6941ce4430152c9ff")},
		{382320, newHashFromStr("00000000000000000a8dc6ed5b133d0eb2fd6af56203e4159789b092defd8ab2")},
		{400000, newHashFromStr("000000000000000004ec466ce4732fe6f1ed1cddc2ed4b328fff5224276e3f6f")},
		{430000, newHashFromStr("000000000000000001868b2bb3a285f3cc6b33ea234eb70facf4dcdf22186b87")},
		{460000, newHashFromStr("000000000000000000ef751bbce8e744ad303c47ece06c8d863e4d417efc258c")},
		{490000, newHashFromStr("000000000000000000de069137b17b8d5a3dfbd5b145b2dcfb203f15d0c4de90")},
		{520000, newHashFromStr("0000000000000000000d26984c0229c9f6962dc74db0a6d525f2f1640396f69c")},
		{550000, newHashFromStr("000000000000000000223b7a2298fb1c6c75fb0efc28a4c56853ff4112ec6bc9")},
		{560000, newHashFromStr("0000000000000000002c7b276daf6efb2b6aa68e2ce3be67ef925b3264ae7122")},
	},

	// 下面还有很多配置项,就不罗列了
	
}

所以从以上知道,节点在运行起来的初始化的时候,检查点列表就被固定了,用高度作为map索引,加快检查点的查找。所有逻辑都是围绕这个两个数据结构来的。

下面我们来看checkpoints.go里面实现了检查点相关的什么函数。

// 返回检查点的列表,大部分被检查点模块内部调用
Checkpoints();
// 返回最新的一个检查点,非关注的,先不管
LatestCheckpoint();
// 校验检查点,看看特定高度的块的Hash是否与检查点相同,不一样就返回false,其他都为true
verifyCheckpoint(height int32, hash *chainhash.Hash) bool// 找到最近的检查点关联的区块实体,该区块实体包括,timestamp, nonce,version, merkle root等。如果找不到对应的区块实体就返回nil
findPreviousCheckpoint() (*blockNode, error);
// 判断传递进去的一个块是否可以作为一个检查点的候选,通过这个函数筛选了,如果返回true,就说明该块适合作为检查点,每到一定的阶段运行这个函数的脚本,把检查点筛选出来添加进配置并硬编码进去,因为我发现这个函数只有一个工具程序的main函数调用了
IsCheckpointCandidate(block *btcutil.Block) (bool, error);

接下来我们再继续分析这些检查点相关的函数的调用栈,也就是说被哪些模块调用了,哪些模块会需要检查点,以及业务上如何处理的:

  • Checkpoints();

该函数基本受checkpoints内部的函数(LatestCheckpoint,verifyCheckpoint,findPreviousCheckpoint)调用。不是重要关注点。

  • LatestCheckpoint();

BlockChain::isCurrent() -> LatestCheckpoint() 当前链的最高高度与块检查点最高的高度比较,如果小,isCurrent就返回false,暂时不是我们的关注点

BlockChain::checkConnectBlock() -> LatestCheckpoint() 如果当前链的最高高度的块与块检查点最高的高度比较,如果小于等于就关闭BTC里面的运行脚本(runscript),显然也不是我们的关注点

  • verifyCheckpoint()

BlockChain::ProcessBlock -> BlockChain::maybeAcceptBlock -> BlockChain::checkBlockContext -> BlockChain::checkBlockHeaderContext -> BlockChain::verifyCheckpoint()

首先来说下上面的调用栈,ProcessBlock可以认为是P2P网络同步过来的区块,就进入到ProcessBlock方法里面。经过一些上下文独立的检验后,认为可能可以接受这个块进入链了,就会调用maybeAcceptBlock,而该函数里面还会有一些校验,比如校验该块的前序块,校验通过才会继续调用checkBlockContext继续往下校验。checkBlockContext首先会调用checkBlockHeaderContext来校验“块头部”上下文,如果校验出错,直接返回错误,checkBlockHeaderContext函数从块头拿到块的Hash,并拿到校验块的高度,调用verifyCheckpoint函数校验检查点的Hash是否匹配,如果不匹配就返回错误,如果找不到,就返回正确,继往下走。

以下是该函数调用后的所做的相关业务逻辑处理的代码:

// Ensure chain matches up to predetermined checkpoints.
	blockHash := header.BlockHash()
	if !b.verifyCheckpoint(blockHeight, &blockHash) {
		str := fmt.Sprintf("block at height %d does not match "+
			"checkpoint hash", blockHeight)
		return ruleError(ErrBadCheckpoint, str)
	}

verifyCheckPoint实现:

// verifyCheckpoint returns whether the passed block height and hash combination
// match the checkpoint data.  It also returns true if there is no checkpoint
// data for the passed block height.
func (b *BlockChain) verifyCheckpoint(height int32, hash *chainhash.Hash) bool {
	if !b.HasCheckpoints() {
		return true
	}

	// Nothing to check if there is no checkpoint data for the block height.
	checkpoint, exists := b.checkpointsByHeight[height]
	if !exists {
		return true
	}

	if !checkpoint.Hash.IsEqual(hash) {
		return false
	}

	log.Infof("Verified checkpoint at height %d/block %s", checkpoint.Height,
		checkpoint.Hash)
	return true
}

看以上代码就知道,几乎是很简单的检查点匹配判断,然后报错。

  • findPreviousCheckpoint() (*blockNode, error);

BlockChain::ProcessBlock -> BlockChain::findPreviousCheckpoint()

BlockChain::verifyCheckpoint() -> BlockChain::findPreviousCheckpoint()

以下就是各个函数调用findPreviousCheckPoint的业务处理

checkpointNode, err := b.findPreviousCheckpoint()
if err != nil {
    return false, false, err
}
if checkpointNode != nil {
    // Ensure the block timestamp is after the checkpoint timestamp.
    checkpointTime := time.Unix(checkpointNode.timestamp, 0)
    if blockHeader.Timestamp.Before(checkpointTime) {
        str := fmt.Sprintf("block %v has timestamp %v before "+
            "last checkpoint timestamp %v", blockHash,
            blockHeader.Timestamp, checkpointTime)
        return false, false, ruleError(ErrCheckpointTimeTooOld, str)
    }
}

意思是这样,如果找不到前序检查点关联的块实体那么肯定是错的,因为没有检查点关联的块,你的链就相当于不是我这条链了,错误。

找到前序检查点关联的块实体,还要确定当前块的时间戳是大于前序检查点的时间戳的,不然也有问题,我作为后续的块的时间戳不应该比前序的时间戳小。这样不符合逻辑。

检查点的选取规则

根据之前的小节分析,检查点是通过一个额外的工具分析出来的,然后选取出来再逐步添加进主链中硬编码,随着版本的发布新增检查点硬编码,

以下是btcd的注释文档:

// IsCheckpointCandidate returns whether or not the passed block is a good
// checkpoint candidate.
//
// The factors used to determine a good checkpoint are:
//  - The block must be in the main chain
//  - The block must be at least 'CheckpointConfirmations' blocks prior to the
//    current end of the main chain
//  - The timestamps for the blocks before and after the checkpoint must have
//    timestamps which are also before and after the checkpoint, respectively
//    (due to the median time allowance this is not always the case)
//  - The block must not contain any strange transaction such as those with
//    nonstandard scripts
//
// The intent is that candidates are reviewed by a developer to make the final
// decision and then manually added to the list of checkpoints for a network.

这里主要理解检查点选取的逻辑。主要是IsCheckpointCandidate(block *btcutil.Block) (bool, error)这个函数判断的,然后我们逐步分析它的调用栈。

先看它的实现,实现代码暂时不贴了,主要分析作为候选检查点的区块的必要条件:

  • 检查点必须在主链上,因为链会硬分叉,说通俗点就是检查点必须在当前最长链上

  • 检查点的高度必须与检查点所对应的Hash的块高一致

  • 检查点的块必须要至少有一定的确认数,btcd里面是至少2016次确认数的块才能入选

// CheckpointConfirmations is the number of blocks before the end of the current
// best block chain that a good checkpoint candidate must be.
const CheckpointConfirmations = 2016

// A checkpoint must be at least CheckpointConfirmations blocks
// before the end of the main chain.
mainChainHeight := b.bestChain.Tip().height
if node.height > (mainChainHeight - CheckpointConfirmations) {
    return false, nil
}
  • 检查点块的时间戳必须在夹在前一个块和后一个块的中间。

main() -> findCandidates() -> IsCheckpointCandidate()

以上就是IsCheckpointCandidate的调用栈,用工具筛选出,然后再公布出来大家一起讨论筛选出检查点,最后再硬编码进主链的代码里。

BigbangCore的检查点方案

  • 筛选检查点的方法为了方便,暂时可以不用做成额外的工具,可以做成RPC列出来,然后人工筛选。或者初期主链高度不高,手工筛选都可以,反正高度都是跳着来的,也不多,人力可以完成。

  • 检查点列表也硬编码进代码里。

  • 仿照BTCD,主要检查点检查的调用也只处理P2P的块,就不用检查已同步的块的了,因为最新的检查点的块的Hash和高度确定,就代表它之前的所有区块的Hash,Timestamp确定了。也就是说,最终如果不匹配还是会报错

参考资料

@AlexiaChen AlexiaChen added the documentation Improvements or additions to documentation label Dec 18, 2019
@AlexiaChen AlexiaChen changed the title 检查点(CheckPoints)调研方案 检查点(CheckPoints)调研报告 Dec 18, 2019
@zhihuizhilv
Copy link

调研非常详细,赞!
补充一点想法,请参考:

  • 检查点的作用是帮助新接入p2p的节点选择正确的主链(保持在线的节点通过共识识别主链),另外还可以实现硬分叉(有重大升级时,指定官方支持的分支)。处理p2p的块可以保证节点从网络同步正确的主链。对于本地已有数据,建议在启动时检测能检测的最新检查点,如果不匹配可以直接提示用户清除数据重新同步或者调整同步起点。
  • 小工具可以用nodejs或者python通过rpc访问节点数据,按照规则提取block hash。提取多个节点数据进行对比,形成最终数据。小工具的逻辑还是比较清晰简单的,工作量不大。如果发布检查点版本的时间不紧急,建议优先制作小工具。

@shangqd
Copy link
Contributor

shangqd commented Dec 19, 2019

调研的很详细,提的建议也挺好

@AlexiaChen
Copy link
Contributor Author

在checkpoint分支已经实现该功能。但是没合并,暂时关闭。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

3 participants