Skip to content

Commit

Permalink
blockchain: Allow adding additional checkpoints
Browse files Browse the repository at this point in the history
Introduces a `--checkpoint` flag that allows the user to specify
additional checkpoints or override the default ones provided in the
chain params.
stevenroose committed Jan 18, 2017
1 parent 8caa921 commit c2af640
Showing 10 changed files with 161 additions and 71 deletions.
21 changes: 14 additions & 7 deletions blockchain/chain.go
Original file line number Diff line number Diff line change
@@ -158,6 +158,7 @@ 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
@@ -188,8 +189,7 @@ type BlockChain struct {

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

// These fields are related to the memory block index. They are
// protected by the chain lock.
@@ -1569,7 +1569,7 @@ func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block, fla
func (b *BlockChain) isCurrent() bool {
// Not current if the latest main (best) chain height is before the
// latest known good checkpoint (when checkpoints are enabled).
checkpoint := b.latestCheckpoint()
checkpoint := b.LatestCheckpoint()
if checkpoint != nil && b.bestNode.height < checkpoint.Height {
return false
}
@@ -1640,6 +1640,12 @@ type Config struct {
// This field is required.
ChainParams *chaincfg.Params

// Checkpoints hold caller-defined checkpoints that should be added to the
// default checkpoints in ChainParams. Checkpoints must be sorted by height.
//
// This field can be nil if the caller did not specify any checkpoints.
Checkpoints []chaincfg.Checkpoint

// TimeSource defines the median time source to use for things such as
// block processing and determining whether or not the chain is current.
//
@@ -1688,20 +1694,21 @@ func New(config *Config) (*BlockChain, error) {
}

// Generate a checkpoint by height map from the provided checkpoints.
params := config.ChainParams
var checkpointsByHeight map[int32]*chaincfg.Checkpoint
if len(params.Checkpoints) > 0 {
if len(config.Checkpoints) > 0 {
checkpointsByHeight = make(map[int32]*chaincfg.Checkpoint)
for i := range params.Checkpoints {
checkpoint := &params.Checkpoints[i]
for i := range config.Checkpoints {
checkpoint := &config.Checkpoints[i]
checkpointsByHeight[checkpoint.Height] = checkpoint
}
}

params := config.ChainParams
targetTimespan := int64(params.TargetTimespan)
targetTimePerBlock := int64(params.TargetTimePerBlock)
adjustmentFactor := params.RetargetAdjustmentFactor
b := BlockChain{
checkpoints: config.Checkpoints,
checkpointsByHeight: checkpointsByHeight,
db: config.DB,
chainParams: params,
8 changes: 2 additions & 6 deletions blockchain/chain_test.go
Original file line number Diff line number Diff line change
@@ -43,9 +43,7 @@ func TestHaveBlock(t *testing.T) {
}
defer teardownFunc()

// Since we're not dealing with the real block chain, disable
// checkpoints and set the coinbase maturity to 1.
chain.DisableCheckpoints(true)
// Since we're not dealing with the real block chain, set the coinbase maturity to 1.
chain.TstSetCoinbaseMaturity(1)

for i := 1; i < len(blocks); i++ {
@@ -131,9 +129,7 @@ func TestCalcSequenceLock(t *testing.T) {
}
defer teardownFunc()

// Since we're not dealing with the real block chain, disable
// checkpoints and set the coinbase maturity to 1.
chain.DisableCheckpoints(true)
// Since we're not dealing with the real block chain, set the coinbase maturity to 1.
chain.TstSetCoinbaseMaturity(1)

// Load all the blocks into our test chain.
72 changes: 20 additions & 52 deletions blockchain/checkpoints.go
Original file line number Diff line number Diff line change
@@ -27,66 +27,39 @@ func newHashFromStr(hexStr string) *chainhash.Hash {
return hash
}

// DisableCheckpoints provides a mechanism to disable validation against
// checkpoints which you DO NOT want to do in production. It is provided only
// for debug purposes.
//
// This function is safe for concurrent access.
func (b *BlockChain) DisableCheckpoints(disable bool) {
b.chainLock.Lock()
b.noCheckpoints = disable
b.chainLock.Unlock()
}

// Checkpoints returns a slice of checkpoints (regardless of whether they are
// already known). When checkpoints are disabled or there are no checkpoints
// for the active network, it will return nil.
// already known).
// When there are no checkpoints for the chain, it will return nil.
//
// This function is safe for concurrent access.
func (b *BlockChain) Checkpoints() []chaincfg.Checkpoint {
b.chainLock.RLock()
defer b.chainLock.RUnlock()

if b.noCheckpoints || len(b.chainParams.Checkpoints) == 0 {
return nil
}

return b.chainParams.Checkpoints
return b.checkpoints
}

// latestCheckpoint returns the most recent checkpoint (regardless of whether it
// is already known). When checkpoints are disabled or there are no checkpoints
// for the active network, it will return nil.
// HasCheckpoints returns whether this BlockChain has checkpoints defined.
//
// This function MUST be called with the chain state lock held (for reads).
func (b *BlockChain) latestCheckpoint() *chaincfg.Checkpoint {
if b.noCheckpoints || len(b.chainParams.Checkpoints) == 0 {
return nil
}

checkpoints := b.chainParams.Checkpoints
return &checkpoints[len(checkpoints)-1]
// This function is safe for concurrent access.
func (b *BlockChain) HasCheckpoints() bool {
return len(b.checkpoints) > 0
}

// LatestCheckpoint returns the most recent checkpoint (regardless of whether it
// is already known). When checkpoints are disabled or there are no checkpoints
// for the active network, it will return nil.
// LatestCheckpoint returns the most recent checkpoint (regardless of whether they
// are already known). When there are no defined checkpoints for the active chain
// instance, it will return nil.
//
// This function is safe for concurrent access.
func (b *BlockChain) LatestCheckpoint() *chaincfg.Checkpoint {
b.chainLock.RLock()
checkpoint := b.latestCheckpoint()
b.chainLock.RUnlock()
return checkpoint
if !b.HasCheckpoints() {
return nil
}
return &b.checkpoints[len(b.checkpoints)-1]
}

// verifyCheckpoint returns whether the passed block height and hash combination
// match the hard-coded checkpoint data. It also returns true if there is no
// checkpoint data for the passed block height.
//
// This function MUST be called with the chain lock held (for reads).
// 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.noCheckpoints || len(b.chainParams.Checkpoints) == 0 {
if !b.HasCheckpoints() {
return true
}

@@ -112,14 +85,14 @@ func (b *BlockChain) verifyCheckpoint(height int32, hash *chainhash.Hash) bool {
//
// This function MUST be called with the chain lock held (for reads).
func (b *BlockChain) findPreviousCheckpoint() (*btcutil.Block, error) {
if b.noCheckpoints || len(b.chainParams.Checkpoints) == 0 {
if !b.HasCheckpoints() {
return nil, nil
}

// No checkpoints.
checkpoints := b.chainParams.Checkpoints
checkpoints := b.checkpoints
numCheckpoints := len(checkpoints)
if numCheckpoints == 0 {
// No checkpoints.
return nil, nil
}

@@ -261,11 +234,6 @@ func (b *BlockChain) IsCheckpointCandidate(block *btcutil.Block) (bool, error) {
b.chainLock.RLock()
defer b.chainLock.RUnlock()

// Checkpoints must be enabled.
if b.noCheckpoints {
return false, fmt.Errorf("checkpoints are disabled")
}

var isCandidate bool
err := b.db.View(func(dbTx database.Tx) error {
// A checkpoint must be in the main chain.
1 change: 1 addition & 0 deletions blockchain/common_test.go
Original file line number Diff line number Diff line change
@@ -116,6 +116,7 @@ func chainSetup(dbName string, params *chaincfg.Params) (*blockchain.BlockChain,
chain, err := blockchain.New(&blockchain.Config{
DB: db,
ChainParams: &paramsCopy,
Checkpoints: nil,
TimeSource: blockchain.NewMedianTime(),
SigCache: txscript.NewSigCache(1000),
})
4 changes: 1 addition & 3 deletions blockchain/reorganization_test.go
Original file line number Diff line number Diff line change
@@ -52,9 +52,7 @@ func TestReorganization(t *testing.T) {
}
defer teardownFunc()

// Since we're not dealing with the real block chain, disable
// checkpoints and set the coinbase maturity to 1.
chain.DisableCheckpoints(true)
// Since we're not dealing with the real block chain set the coinbase maturity to 1.
chain.TstSetCoinbaseMaturity(1)

expectedOrphans := map[int]struct{}{5: {}, 6: {}}
2 changes: 1 addition & 1 deletion blockchain/validate.go
Original file line number Diff line number Diff line change
@@ -1107,7 +1107,7 @@ func (b *BlockChain) checkConnectBlock(node *blockNode, block *btcutil.Block, vi
// will therefore be detected by the next checkpoint). This is a huge
// optimization because running the scripts is the most time consuming
// portion of block handling.
checkpoint := b.latestCheckpoint()
checkpoint := b.LatestCheckpoint()
runScripts := !b.noVerify
if checkpoint != nil && node.height <= checkpoint.Height {
runScripts = false
62 changes: 60 additions & 2 deletions blockmanager.go
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import (
"net"
"os"
"path/filepath"
"sort"
"sync"
"sync/atomic"
"time"
@@ -182,7 +183,7 @@ func (b *blockManager) findNextHeaderCheckpoint(height int32) *chaincfg.Checkpoi
if cfg.DisableCheckpoints {
return nil
}
checkpoints := b.server.chainParams.Checkpoints
checkpoints := b.chain.Checkpoints()
if len(checkpoints) == 0 {
return nil
}
@@ -1324,6 +1325,57 @@ func (b *blockManager) Pause() chan<- struct{} {
return c
}

// checkpointSorter implements sort.Interface to allow a slice of checkpoints to
// be sorted.
type checkpointSorter []chaincfg.Checkpoint

// Len returns the number of checkpoints in the slice. It is part of the
// sort.Interface implementation.
func (s checkpointSorter) Len() int {
return len(s)
}

// Swap swaps the checkpoints at the passed indices. It is part of the
// sort.Interface implementation.
func (s checkpointSorter) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}

// Less returns whether the checkpoint with index i should sort before the
// checkpoint with index j. It is part of the sort.Interface implementation.
func (s checkpointSorter) Less(i, j int) bool {
return s[i].Height < s[j].Height
}

// mergeCheckpoints returns two slices of checkpoints merged into one slice
// such that the checkpoints are sorted by height. In the case the additional
// checkpoints contain a checkpoint with the same height as a checkpoint in the
// default checkpoints, the additional checkpoint will take precedence and
// overwrite the default one.
func mergeCheckpoints(defaultCheckpoints, additional []chaincfg.Checkpoint) []chaincfg.Checkpoint {
// Create a map of the additional checkpoint heights to detect
// duplicates.
additionalHeights := make(map[int32]struct{})
for _, checkpoint := range additional {
additionalHeights[checkpoint.Height] = struct{}{}
}

// Add all default checkpoints that do not have an override in the
// additional checkpoints.
numDefault := len(defaultCheckpoints)
checkpoints := make([]chaincfg.Checkpoint, 0, numDefault+len(additional))
for _, checkpoint := range defaultCheckpoints {
if _, exists := additionalHeights[checkpoint.Height]; !exists {
checkpoints = append(checkpoints, checkpoint)
}
}

// Append the additional checkpoints and return the sorted results.
checkpoints = append(checkpoints, additional...)
sort.Sort(checkpointSorter(checkpoints))
return checkpoints
}

// newBlockManager returns a new bitcoin block manager.
// Use Start to begin processing asynchronous block and inv updates.
func newBlockManager(s *server, indexManager blockchain.IndexManager) (*blockManager, error) {
@@ -1338,11 +1390,18 @@ func newBlockManager(s *server, indexManager blockchain.IndexManager) (*blockMan
quit: make(chan struct{}),
}

// Merge given checkpoints with the default ones unless they are disabled.
var checkpoints []chaincfg.Checkpoint
if !cfg.DisableCheckpoints {
checkpoints = mergeCheckpoints(s.chainParams.Checkpoints, cfg.addCheckpoints)
}

// Create a new block chain instance with the appropriate configuration.
var err error
bm.chain, err = blockchain.New(&blockchain.Config{
DB: s.db,
ChainParams: s.chainParams,
Checkpoints: checkpoints,
TimeSource: s.timeSource,
Notifications: bm.handleNotifyMsg,
SigCache: s.sigCache,
@@ -1352,7 +1411,6 @@ func newBlockManager(s *server, indexManager blockchain.IndexManager) (*blockMan
return nil, err
}
best := bm.chain.BestSnapshot()
bm.chain.DisableCheckpoints(cfg.DisableCheckpoints)
if !cfg.DisableCheckpoints {
// Initialize the next checkpoint based on the current height.
bm.nextCheckpoint = bm.findNextHeaderCheckpoint(best.Height)
Loading

0 comments on commit c2af640

Please sign in to comment.