btcd源码解析——peer节点之间的区块数据同步 (4) —— 区块数据的存储

1. 写在前面

前两篇博客headersFirstMode模式非headersFirstMode模式主要介绍了区块数据同步过程中两个节点如何交互。但我们当时遗留下一个问题:在peer A接收到区块数据之后,是如何将这个区块存储到本地的。回忆我们在headersFirstMode模式的2.6小节最后提到的

由于block的数据存储过程比较复杂,我们在后面会再写一篇博客介绍:从P2P连接中接收到的block数据是如何存储的

本篇博客主要就来介绍“区块数据的存储过程”。

2. 区块数据的存储过程

回忆我们在headersFirstMode模式的2.6小节最后提到的blockHandler函数,其代码如下所示:

// inHandler [peer.go] -> OnBlock [server.go] -> QueueBlock [manager.go] -> blockHandler
func (sm *SyncManager) blockHandler() {
    ...
out: 
    for { 
        select { 
        case m := <-sm.msgChan:       
            switch msg := m.(type) {       
            ...
            case *blockMsg:       
                sm.handleBlockMsg(msg)           			      // L1289
                msg.reply <- struct{}{}
            ...
            } 
        } 
        ...
}

peer A接收到区块数据后,主要在L1289行调用了handleBlockMsg函数来处理该数据。以下就是从handleBlockMsg函数开始介绍区块数据的存储过程。

2.1 SyncManager数据结构

SyncManager这个类型我们已经见过很多次了,这里我们先来看一下SyncManager类型的定义:

// SyncManager [manager.go]
type SyncManager struct {                       // L155
    ...
    msgChan                chan interface{}
    ...
    requestedBlocks       map[chainhash.Hash]struct{}
    syncPeer                 *peerpkg.Peer
    peerStates              map[*peerpkg.Peer]*peerSyncState
    ...
    headersFirstMode    bool
    headerList              *list.List
    startHeader            *list.Element
    nextCheckpoint       *chaincfg.Checkpoint
    ...
}

上面的代码中主要列出了需要重点解释的字段。

  • msgchan:我们在前几篇博客中多次提到该字段,该字段主要用于将P2P连接中接收到的数据传递给blockHandler函数进行处理,具体可见博客btcd源码解析(1)的第2小节和btcd源码解析(2)的第2.3小节
  • requestedBlocks:该字段主要用于记录哪些区块处于"requested"状态。每次发出一个获取区块的请求前,先将该区块的hash加入到requestedBlocks中;每次收到区块后,将该区块的hashrequestedBlocks中删除
  • syncPeer:该字段主要用于标识该节点(peer A)正在用于数据同步的peer B. 任何时刻,只存在一个用于同步的syncPeer。每次会选出最佳的peer B赋值给syncPeer(见btcd源码解析(1)的第2小节),且在peer B失去连接等情况发生时,syncPeer会被切换为其他peer
  • peerStates:该字段主要用于记录所有peer的同步状态。因为syncPeer会在不同peer之间切换,需要记录下来跟每个peer进行数据同步的状态
  • headersFirstMode:该字段表明当前是否采用headersFirstMode同步模式
  • headerList:该字段只在headersFirstMode模式下有意义。由于在headersFirstMode模式下,headersblocks都是按阶段下载的。checkpoint将所有的区块分成若干个区间,每个阶段下载其中一个区间的header (block),完成后再下载下一个区间
  • startHeader:该字段只在headersFirstMode模式下有意义。startHeader标识当前正在处理的checkpoint区间中,还未发出block获取请求的第一个header
  • nextCheckpoint:该字段只在headersFirstMode模式下有意义。nextCheckpoint标识当前正在处理的checkpoint区间的下边界

2.2 handleBlockMsg函数分析

handleBlockMsg函数的定义如下所示:

// inHandler [peer.go] -> OnBlock[server.go]->QueueBlock[manager.go] 
// -> blockHandler -> handleBlockMsg
func (sm *SyncManager) handleBlockMsg(bmsg *blockMsg) {
    ...
    isCheckpointBlock := false   							// L660
    behaviorFlags := blockchain.BFNone
    if sm.headersFirstMode {      
        firstNodeEl := sm.headerList.Front()      
        if firstNodeEl != nil {            
            firstNode := firstNodeEl.Value.(*headerNode)            
            if blockHash.IsEqual(firstNode.hash) {                  
                behaviorFlags |= blockchain.BFFastAdd                  
                if firstNode.hash.IsEqual(sm.nextCheckpoint.Hash){                      
                    isCheckpointBlock = true   
                }else{                        
                    sm.headerList.Remove(firstNodeEl)                  
                }            
            }      
        }
    }  						                              // L675
    ...
    _, isOrphan, err := sm.chain.ProcessBlock(bmsg.block, behaviorFlags) // L723 
    ...
    if !sm.headersFirstMode {  			                  // L784
        return
    }   		                                          // L786
    ...
    if !isCheckpointBlock {    			                  // L791
        if sm.startHeader != nil &&            
            len(state.requestedBlocks) < minInFlightBlocks{            
            sm.fetchHeaderBlocks()      
        }      
        return
    }  								                      // L797
    ...
    prevHeight := sm.nextCheckpoint.Height				  // L803
    prevHash := sm.nextCheckpoint.Hash
    sm.nextCheckpoint = sm.findNextHeaderCheckpoint(prevHeight)
    if sm.nextCheckpoint != nil {
        locator :=blockchain.BlockLocator([]*chainhash.Hash{prevHash})
        err := peer.PushGetHeadersMsg(locator, sm.nextCheckpoint.Hash)
        ...
        return 
    }      						                          // L818
    ...
    sm.headersFirstMode = false                           // L823
    sm.headerList.Init()
    ...
    locator := blockchain.BlockLocator([]*chainhash.Hash{blockHash})
    err = peer.PushGetBlocksMsg(locator, &zeroHash)       // L827
    ...
}

该函数的信息量非常大,我们先大概看一下整个函数的脉络:

  • L660-L675: 根据获得的block数据,对一些变量进行赋值
  • L723: 调用ProcessBlock函数,该函数完成了区块处理最主要的功能,包括:区块数据的验证、处理orphan区块、将区块插入到链中、基于新插入的区块判断是否需要reorganize最长链
  • L784-L786: 如果当前是正常的区块下载模式 (非headersFirstMode模式),直接返回
  • L791-L797: 如果当前区块不是checkpoint区块,且当前区间中仍有未发出block获取请求的区块,且已发出获取请求但未接收到响应的区块个数小于minInFlightBlocks,继续调用fetchHeaderBlocks函数发出新的区块请求。fetchHeaderBlocks函数的定义见博客btcd源码解析(2)的2.4小节。
  • L803-L818: 如果当前处于headersFirstMode模式,且当前区块是checkpoint区块,且存在下一个checkpoint,说明这一个区间的headerblock下载已经完成了,可以开始下一个区间的下载了。下一个区间的下载和之前类似,具体流程可参看博客btcd源码解析(2)
  • L823-L827: 如果当前处于headersFirstMode模式,且当前区块是checkpoint区块,且不存在下一个checkpoint,则切换到正常的区块下载模式 (非headersFirstMode模式),具体流程可参看博客btcd源码解析(3)

2.3 ProcessBlock函数分析

这一节,我们重点就来分析一下ProcessBlock函数,该函数定义如下所示:

// inHandler [peer.go] -> OnBlock [server.go] -> 
// QueueBlock [manager.go] -> blockHandler -> handleBlockMsg -> 
// ProcessBlock [process.go]
func (b *BlockChain) ProcessBlock(block *btcutil.Block, flags 
BehaviorFlags) (bool, bool, error) {
    ...
    err = checkBlockSanity(...) 							// L168
    ...
    blockHeader := &block.MsgBlock().Header                             
    ...
    prevHash := &blockHeader.PrevBlock  					// L214
    prevHashExists, err := b.blockExists(prevHash)
    ...
    if !prevHashExists {      
        ...      
        b.addOrphanBlock(block) 			                // L221
        return false, true, nil
    }           			                                // L224
    ...
    isMainChain, err := b.maybeAcceptBlock(block, flags)    // L228
    ...
    err = b.processOrphans(blockHash, flags)    			// L236
    ...
    return isMainChain, false, nil
}

L214-L224: 首先判断当前区块的prevHash是否存在。如果不存在,说明该区块是orphan,通过L221行的addOrphanBlock函数,将该block加入到orphans字典中
L228: maybeAcceptBlock函数将block加入到链中,并返回一个bool值标识该block是否在最长链中
L236: processOrphans函数尝试利用该block将之前的orphan串起来。具体而言,如果之前的block正好是因为缺少当前的这个block而被判别为了orphan(因为整个链断开了),那么当当前这个block到来时,便可以将之前的orphan都串起来了。

2.4 maybeAcceptBlock函数分析

这一节我们来分析一下maybeAcceptBlock函数,该函数定义如下:

// inHandler [peer.go] -> OnBlock [server.go] -> QueueBlock 
// [manager.go] -> blockHandler -> handleBlockMsg -> ProcessBlock 
// [process.go] -> maybeAcceptBlock [accept.go]
func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block, flags 
BehaviorFlags) (bool, error) {
    ...
    err = b.db.Update(func(dbTx database.Tx) error {  		// L56
        return dbStoreBlock(dbTx, block)
    })                                                      // L58
    ...
    blockHeader := &block.MsgBlock().Header 	            // L66
    newNode := newBlockNode(blockHeader, prevNode)
    newNode.status = statusDataStored
    
    b.index.AddNode(newNode)
    err = b.index.flushToDB()   							// L71
    ...
    isMainChain, err := b.connectBestChain(newNode, block, flags) //L79
    ...
    return isMainChain, nil
}

L56-L58: 将该区块存储到数据库中
L66-L71: 首先需要解释一下indexnode的作用,node可以认为是block的代表,使得block信息的查询更加快捷;index用于保存这些node。L66-L71行代码基于该区块构建了一个node,并将该node加入到index中,最后将该index刷新到数据库中。
L79:通过maybeAcceptBlock的若干检查之后,真正地将block写入到区块链中

2.5 connectBestChain函数分析

本小节主要分析connectBestChain函数,该函数代码如下所示:

// inHandler [peer.go] -> OnBlock [server.go] -> QueueBlock
// [manager.go] -> blockHandler -> handleBlockMsg -> ProcessBlock
// [process.go] -> maybeAcceptBlock [accept.go] -> 
// connectBestChain[chain.go]
func (b *BlockChain) connectBestChain(node *blockNode, block 
*btcutil.Block, flags BehaviorFlags) (bool, error) {
    ...
    parentHash := &block.MsgBlock().Header.PrevBlock
    if parentHash.IsEqual(&b.bestChain.Tip().hash) { 		// L1102
        ...
        view := NewUtxoViewpoint() 			                // L1109
        view.SetBestHash(parentHash)
        stxos := make([]SpentTxOut, 0, countSpentOutputs(block)) // L1111
        if !fastAdd {        		                        // L1112
            err := b.checkConnectBlock(node, block, view, &stxos) // L1113
            ...
        }        					                       // L1132
        if fastAdd {     				                   // L1133
            err := view.fetchInputUtxos(b.db, block)       // L1134
            ...
            err = view.connectTransactions(block, &stxos)  // L1138
            ...
        }   						                       // L1142
        ...
        err := b.connectBlock(node, block, view, stxos)    // L1145
        ...
    }         							                   // L1170
    ...
    if node.workSum.Cmp(b.bestChain.Tip().workSum) <= 0 {  // L1178
        ...
        fork := b.bestChain.FindFork(node) 			       // L1180
        if fork.hash.IsEqual(parentHash) {   		       // L1181
            ...
        } else {                 		                   // L1185
            ...
        }                       							// L1189
        
        return false, nil
    }                                                                               // L1192
    ...
    detachNodes, attachNodes := b.getReorganizeNodes(node)  // L1201
    ...
    err := b.reorganizeChain(detachNodes, attachNodes)     // L1205
    ...
}

L1102首先判断当前区块的PrevBlock是否是bestChaintip. 如果是,则表明当前的block延长了main chain. L1102-L1170便是来处理"延长main chain"这种情况的。
具体而言:

  • L1109创建了一个UtxoViewpoint变量 (view)。view主要用来记录某个状态下的utxo的视图
  • L1111创建了一个SpentTxOutslice (stxos)。stxos主要用来记录当前区块花出去的utxo
    分两种情况 (headersFirstMode非headersFirstMode),viewstxos在后续代码中得到了填充。当为非headersFirstMode模式时,viewstxos在L1113行得到了填充;当为headersFirstMode模式时,view在L1134行得到填充,stxos在L1138行得到填充。
    以下先来看一下L1113行的checkConnectBlock函数。
2.5.1 非headersFirstMode模式下填充view和stxos变量
// inHandler [peer.go] -> OnBlock [server.go] -> QueueBlock
// [manager.go] -> blockHandler -> handleBlockMsg -> ProcessBlock
// [process.go] -> maybeAcceptBlock [accept.go] -> connectBestChain
// [chain.go] -> checkConnectBlock [validate.go]
func (b *BlockChain) checkConnectBlock(node *blockNode, block 
*btcutil.Block, view *UtxoViewpoint, stxos *[]SpentTxOut) error {
    ...
    err := view.fetchInputUtxos(b.db, block)            // L1039
    ...
    transactions := block.Transactions()
    ...
    for _, tx := range transactions {                   // L1100
        ...
        err = view.connectTransaction(tx, node.height, stxos)// L1120
        ...
    }          		                                   // L1124
    ...
}

L1039行使用fetchInputUtxos函数对view变量进行填充。总体而言,fetchInputUtxos函数主要是将一个block中所有的input所对应的output加入到view变量中。以下先来看一下fetchInputUtxos函数。

2.5.1.1 fetchInputUtxos函数分析
// inHandler [peer.go] -> OnBlock [server.go] -> QueueBlock 
// [manager.go] -> blockHandler -> handleBlockMsg -> ProcessBlock
// [process.go] -> maybeAcceptBlock [accept.go] -> connectBestChain
// [chain.go] -> checkConnectBlock [validate.go] -> fetchInputUtxos
// [utxoviewpoint.go]
func (view *UtxoViewpoint) fetchInputUtxos(db database.DB, block 
*btcutil.Block) error {
    ...
    txInFlight := map[chainhash.Hash]int{}   		          // L533
    transactions := block.Transactions()
    for i, tx := range transactions {      
        txInFlight[*tx.Hash()] = i
    }                                  						  // L537
    ... 
    neededSet := make(map[wire.OutPoint]struct{}) 		      // L542
    for i, tx := range transactions[1:] {                     // L543
        for _, txIn := range tx.MsgTx().TxIn {
            ...
            originHash := &txIn.PreviousOutPoint.Hash         // L556
            if inFlightIndex, ok := txInFlight[*originHash]; ok &&     
                i >= inFlightIndex {      
            
                originTx := transactions[inFlightIndex]      
                view.AddTxOuts(originTx, block.Height())      
                continue
            }               	                             // L563
            ...
            neededSet[txIn.PreviousOutPoint] = struct{}{}
        }
    }                      		                            // L573
    ...
    return view.fetchUtxosMain(db, neededSet)              // L576
}

一个block中的input的来源分为两种情况:

  1. input来源于之前block生成的output (第1种情况)
  2. input来源于当前block之前transaction生成的output (第2种情况)

fetchInputUtxos函数也主要从这两种情况进行处理。

  • L533-L537: 将当前block中的所有transaction加入到txInFlight变量中,方便后续查找
  • L542: 新建了一个neededSet变量,该变量主要用来记录第一种来源的input对应的OutPoint
  • L543-L573: 逐个处理每个非coinbasetransaction。对于每个transaction中的每个input,首先判断该input的来源是哪种情况。若是第2种情况,则基于当前区块中相应的transaction (originalTx),利用AddTxOuts函数构建output并加入到view变量中 (见L556-L563)。若是第一种情况,则将该input对应的OutPoint加入到neededSet变量中
  • L576: 基于neededSet,将OutPoint对应的output加入到view变量中。因为需要从数据库中查询,所以需要传入db参数。
2.5.1.2 connectTransaction函数分析

回到 2.5.1小节的checkConnectBlock函数,L1120行采用connectTransaction函数对view变量进行了更改,并对stxos变量进行了填充。以下是connectTransaction函数的定义:

// inHandler [peer.go] -> OnBlock [server.go] -> QueueBlock
// [manager.go] -> blockHandler -> handleBlockMsg -> ProcessBlock
// [process.go] -> maybeAcceptBlock [accept.go] -> connectBestChain
// [chain.go] -> checkConnectBlock [validate.go] -> connectTransaction
// [utxoviewpoint.go]
func (view *UtxoViewpoint) connectTransaction(tx *btcutil.Tx, 
blockHeight int32, stxos *[]SpentTxOut) error {
    ...
    if IsCoinBase(tx) {                                 // L221
        ...
        view.AddTxOuts(tx, blockHeight)      
        return nil
    }              	                                   // L225
    ...
    for _, txIn := range tx.MsgTx().TxIn {             // L230
        ...
        entry := view.entries[txIn.PreviousOutPoint]   // L233
        ...
        if entry == nil {      
            return AssertError(fmt.Sprintf("view missing input %v",            
                txIn.PreviousOutPoint))
        }                                              // L237
        ...
        if stxos != nil {                                                           
            // Populate the stxo details using the utxo entry.      
            var stxo = SpentTxOut{                     // L242
                Amount:     entry.Amount(),            
                PkScript:   entry.PkScript(),            
                Height:     entry.BlockHeight(),            
                IsCoinBase: entry.IsCoinBase(),      
            }      
            *stxos = append(*stxos, stxo)              // L248
        }
        ...
        entry.Spend()    		                       // L254
    }               								   // L255
    ...
    view.AddTxOuts(tx, blockHeight)                    // L258
    return nil
}
  • L221-L225: 如果当前交易是coinbase,则直接将该transaction对应的output加入到view变量中,stxos不变
  • L230-L255: 对每一个TxIn进行处理
  • L233-L237: 如果某个txIn对应的PreviousOutPointview中不存在,说明该input出错无效
  • L242-L248: 根据该txIn对应的output构建stxo,并加入到stxos
  • L254: 更新view中该txIn对应的output,利用Spend函数,将其更新为"已花费"
  • L258: 将该交易中的output加入到变量view
2.5.2 headersFirstMode模式下填充view和stxos变量

回到2.5小节的connectBestChain函数,L1133-L1142行完成了headersFirstMode模式下对viewstxos变量的填充。L1134行的fetchInputUtxos函数,我们已经在2.5.1.1小节进行了讲解,下面看一下L1138行的connectTransactions函数。

2.5.2.1 connectTransactions函数分析
// inHandler [peer.go] -> OnBlock [server.go] -> QueueBlock
// [manager.go] -> blockHandler -> handleBlockMsg -> ProcessBlock
// [process.go] -> maybeAcceptBlock [accept.go] -> connectBestChain
// [chain.go] -> connectTransactions [utxoviewpoint.go]
func (view *UtxoViewpoint) connectTransactions(block *btcutil.Block, 
stxos *[]SpentTxOut) error {
    for _, tx := range block.Transactions() {      
        err := view.connectTransaction(tx, block.Height(), stxos)
        ...
    }
    ...
}

可以看出,connectTransactions只是调用了connectTransaction函数,后者在2.5.1.2小节也已经介绍过,这里不再赘述。

2.5.3 connectBlock函数介绍

回到2.5小节的connectBestChain函数,L1145行调用connectBlock函数将block加入到main chain中,后者的函数定义如下所示:

// inHandler [peer.go] -> OnBlock [server.go] -> QueueBlock
// [manager.go] -> blockHandler -> handleBlockMsg -> ProcessBlock
// [process.go] -> maybeAcceptBlock [accept.go] -> connectBestChain
// [chain.go] -> connectBlock [chain.go]
func (b *BlockChain) connectBlock(...) error {
    ...
    err = b.db.Update(func(dbTx database.Tx) error {
        ...
        err := dbPutBestState(dbTx, state, node.workSum)
        ...
        err = dbPutBlockIndex(dbTx, block.Hash(), node.height)
        ...
        err = dbPutUtxoView(dbTx, view)
        ...
        err = dbPutSpendJournalEntry(dbTx, block.Hash(), stxos)
        ...
        if b.indexManager != nil {      
            err := b.indexManager.ConnectBlock(dbTx, block, stxos)        // L644
            ...
        }
        ...
    })
    ...
}

connectBlock函数的功能主要是把最新的数据更新到数据库中,包括BestState, BlockIndex, UtxoViewSpendJournalEntry. 值得一提的是L644行的ConnectBlock函数,该函数被赋值为Manager.ConnectBlock. btcd的实现中,为了方便block, transaction, address等元素的查找,实现了若干个index,并保存到数据库中。ConnectBlock函数便是用来更新这些index的。

2.5.4 区块分叉的两种情形

回到2.5小节的connectBestChain函数中,L1178-L1192行处理可能的分叉问题。其中L1181行的分支在当前区块的前一个区块处分叉,所以第一次形成了一条side chain,如下图左图所示;L1185行的分支在已有的side chain上延长,如下图右图所示。
区块分叉示意图

2.5.5 reorganizeChain的过程分析

回到2.5小节的connectBestChain函数中。当函数运行到L1202行时,说明新block的加入导致了side chain (分叉链) 的算力超过了main chain,需要用side chain替换现有的main chain,这个过程称为reorganizeChain.
总体而言,reorganizeChain的过程需要将旧的main chain中的block对链的影响进行回滚,并施加新的main chain中的block对链的影响。
由于并不需要将旧链中的所有block进行回滚,只需要回滚分叉点之后的block。如上图右图所示,只需要回滚编号k之后的block. 同理,只需要添加新链中编号k之后的block. L1202行调用getReorganizeNodes函数得到需要回滚的block (detachNodes)和需要添加的block (attachNodes).
L1205行基于detachNodesattachNodes对链的状态进行重新组织。重新组织的过程是调用reorganizeChain函数实现的。reorganizeChain函数主要功能分为两个部分:

  • 回滚detachNodes中区块的操作,可以看作2.5.1或2.5.2小节的逆过程,这里不再赘述
  • 添加attachNodes中的区块,和2.5.1或2.5.2小节类似,这里也不再赘述。

3. 小节

至此,我们介绍完了peer节点之间数据同步过程中的数据到底是怎么存储的。
总体而言,主体的函数调用路径是:

handleBlockMsg -> ProcessBlock [process.go] -> maybeAcceptBlock [accept.go] -> connectBestChain [chain.go] -> connectBlock [chain.go]

数据的存储也包括五个部分:BestState, BlockIndex, UtxoView, SpendJournalEntryIndex.

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
2008年爆发全球金融危机,同年11月1日,一个自称中本聪(Satoshi Nakamoto)的人在P2P foundation网站上发布了比特币白皮书《比特币:一种点对点的电子现金系统》 [6]  ,陈述了他对电子货币的新设想——比特币就此面世。2009年1月3日,比特币创世区块诞生。 和法定货币相比,比特币没有一个集中的发行方,而是由网络节点的计算生成,谁都有可能参与制造比特币,而且可以全世界流通,可以在任意一台接入互联网的电脑上买卖,不管身处何方,任何人都可以挖掘、购买、出售或收取比特币,并且在交易过程中外人无法辨认用户身份信息。2009年1月5日,不受央行和任何金融机构控制的比特币诞生。比特币是一种数字货币,由计算机生成的一串串复杂代码组成,新比特币通过预设的程序制造。 每当比特币进入主流媒体的视野时,主流媒体总会请一些主流经济学家分析一下比特币。早先,这些分析总是集中在比特币是不是骗局。而现如今的分析总是集中在比特币能否成为未来的主流货币。而这其中争论的焦点又往往集中在比特币的通缩特性上。 [7]  不少比特币玩家是被比特币的不能随意增发所吸引的。和比特币玩家的态度截然相反,经济学家们对比特币2100万固定总量的态度两极分化。 凯恩斯学派的经济学家们认为政府应该积极调控货币总量,用货币政策的松紧来为经济适时的加油或者刹车。因此,他们认为比特币固定总量货币牺牲了可调控性,而且更糟糕的是将不可避免地导致通货紧缩,进而伤害整体经济。奥地利学派经济学家们的观点却截然相反,他们认为政府对货币的干预越少越好,货币总量的固定导致的通缩并没什么大不了的,甚至是社会进步的标志。 比特币网络通过“挖矿”来生成新的比特币。所谓“挖矿”实质上是用计算机解决一项复杂的数学问题,来保证比特币网络分布式记账系统的一致性。比特币网络会自动调整数学问题的难度,让整个网络约每10分钟得到一个合格答案。随后比特币网络会新生成一定量的比特币作为区块奖励,奖励获得答案的人。 [6]  2009年,比特币诞生的时候,区块奖励是50个比特币。诞生10分钟后,第一批50个比特币生成了,而此时的货币总量就是50。随后比特币就以约每10分钟50个的速度增长。当总量达到1050万时(2100万的50%),区块奖励减半为25个。当总量达到1575万(新产出525万,即1050的50%)时,区块奖励再减半为12.5个。该货币系统曾在4年内只有不超过1050万个,之后的总数量将被永久限制在约2100万个。 [3]  [8]  比特币是一种虚拟货币,数量有限,但是可以用来套现:可以兑换成大多数国家的货币。你可以使用比特币购买一些虚拟的物品,比如网络游戏当中的衣服、帽子、装备等,只要有人接受,你也可以使用比特币购买现实生活当中的物品。 2014年2月25日,“比特币中国”的比特币开盘价格为3562.41元,截至下午4点40分,价格已下跌至3185元,跌幅逾10%。根据该平台的历史行情数据显示,在2014年1月27日,1比特币还能兑换5032元人民币。这意味着,该平台上不到一个月,比特币价格已下跌了36.7%。 同年9月9日,美国电商巨头eBay宣布,该公司旗下支付处理子公司Braintree将开始接受比特币支付。该公司已与比特币交易平台Coinbase达成合作,开始接受这种相对较新的支付手段。 虽然eBay市场交易平台和PayPal业务还不接受比特币支付,但旅行房屋租赁社区Airbnb和租车服务Uber等Braintree客户将可开始接受这种虚拟货币。Braintree的主要业务是面向企业提供支付处理软件,该公司在2013年被eBay以大约8亿美元的价格收购。 2017年1月22日晚间,火币网、比特币中国与OKCoin币行相继在各自官网发布公告称,为进一步抑制投机,防止价格剧烈波动,各平台将于1月24日中午12:00起开始收取交易服务费,服务费按成交金额的0.2%固定费率收取,且主动成交和被动成交费率一致。 [9]  5月5日,OKCoin币行网的新数据显示,比特币的价格刚刚再度刷新历史,截止发稿前高触及9222元人民币高位。1月24日中午12:00起,中国三大比特币平台正式开始收取交易费。9月4日,央行等七部委发公告称中国禁止虚拟货币交易。同年12月17日,比特币达到历史高价19850美元。 2018年11月25日,比特币跌破4000美元大关,后稳定在3000多美元。 [10]  11月19日,加密货币恢复跌势,比特币自2017年10月以来首次下探5000美元大关,原因是之前BCH出现硬分叉,且监管部门对首次代币发行(ICO)加强了审查。 [10]  11月21日凌晨4点半,coinbase平台比特币报价跌破4100美元,创下了13个月以来的新低。 2019年4月,比特币再次突破5000美元大关,创年内新高。 [11]  5月12日,比特币近八个月来首次突破7000美元。 [12]  5月14日,据coinmarketcap报价显示,比特币站上8000美元,24小时内上涨14.68%。 [13]  6月22日 ,比特币价格突破10000美元大关。比特币价格在10200左右震荡,24小时涨幅近7%。 [14]  6月26日,比特币价格一举突破12000美元,创下自去年1月来近17个月高点。 [15]  6月27日早间,比特币价格一度接近14000美元,再创年内新高。 [16]  2020年2月10日,比特币突破了一万美元。据交易数据比特币的价格涨幅突破3% [17]  。3月12日,据加密货币交易平台Bitstamp数据显示,19点44分,比特币低价格已跌至5731美元 [18]  。5月8日,比特币突破10000美元关口,创下2月份以来的新高 [19]  。5月10日早上8点开始,比特币单价在半小时内从9500美元价位瞬间下跌了上千美元,低价格跌破8200美元,高价差超1400美元 [20]  。7月26日下午6点,比特币短时极速拉升,高触及10150.15USDT,日内大涨幅超过4%,这是2020年6月2日以来首次突破1万美元关口 [21]  。11月4日,比特币价格正式突破14000美元 [22]  。11月12日晚,比特币价格突破16000美元,刷新2018年1月以来新高,一周涨超8.6%。比特币总市值突破2915亿美元 [23]  。11月18日,比特币价格突破17000美元 [24]  。12月1日,比特币价格报19455.31美元,24小时涨幅为5.05%。 [25]  12月17日,比特币价格突破23000美元整数关口,刷新历史新高,日内涨幅超7.5%。 [26]  截至12月27日19时20分,比特币报价28273.06美元。 [27]  2021年1月8日,比特币涨至4万美元关口上方,高至40402美元

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值