diff --git a/blockchain/chain.go b/blockchain/chain.go index afed2771..d585f108 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -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 := ¶ms.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, diff --git a/blockchain/chain_test.go b/blockchain/chain_test.go index f92d3952..6dbd7ca7 100644 --- a/blockchain/chain_test.go +++ b/blockchain/chain_test.go @@ -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. diff --git a/blockchain/checkpoints.go b/blockchain/checkpoints.go index 7e133977..f0144d8a 100644 --- a/blockchain/checkpoints.go +++ b/blockchain/checkpoints.go @@ -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. diff --git a/blockchain/common_test.go b/blockchain/common_test.go index aec85bfb..21683478 100644 --- a/blockchain/common_test.go +++ b/blockchain/common_test.go @@ -116,6 +116,7 @@ func chainSetup(dbName string, params *chaincfg.Params) (*blockchain.BlockChain, chain, err := blockchain.New(&blockchain.Config{ DB: db, ChainParams: ¶msCopy, + Checkpoints: nil, TimeSource: blockchain.NewMedianTime(), SigCache: txscript.NewSigCache(1000), }) diff --git a/blockchain/reorganization_test.go b/blockchain/reorganization_test.go index bb71fa5a..e543a759 100644 --- a/blockchain/reorganization_test.go +++ b/blockchain/reorganization_test.go @@ -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: {}} diff --git a/blockchain/validate.go b/blockchain/validate.go index 3499fde4..48451787 100644 --- a/blockchain/validate.go +++ b/blockchain/validate.go @@ -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 diff --git a/blockmanager.go b/blockmanager.go index 10d2cfc0..79dbc1b9 100644 --- a/blockmanager.go +++ b/blockmanager.go @@ -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) diff --git a/config.go b/config.go index 1c9b2522..7a97b8c1 100644 --- a/config.go +++ b/config.go @@ -20,6 +20,8 @@ import ( "strings" "time" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/connmgr" "github.com/btcsuite/btcd/database" _ "github.com/btcsuite/btcd/database/ffldb" @@ -123,6 +125,7 @@ type config struct { TestNet3 bool `long:"testnet" description:"Use the test network"` RegressionTest bool `long:"regtest" description:"Use the regression test network"` SimNet bool `long:"simnet" description:"Use the simulation test network"` + AddCheckpoints []string `long:"addcheckpoint" description:"Add a custom checkpoint. Format: ':'"` DisableCheckpoints bool `long:"nocheckpoints" description:"Disable built-in checkpoints. Don't do this unless you know what you're doing."` DbType string `long:"dbtype" description:"Database backend to use for the Block Chain"` Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"` @@ -150,6 +153,7 @@ type config struct { lookup func(string) ([]net.IP, error) oniondial func(string, string, time.Duration) (net.Conn, error) dial func(string, string, time.Duration) (net.Conn, error) + addCheckpoints []chaincfg.Checkpoint miningAddrs []btcutil.Address minRelayTxFee btcutil.Amount } @@ -303,6 +307,49 @@ func normalizeAddresses(addrs []string, defaultPort string) []string { return removeDuplicateAddresses(addrs) } +// newCheckpointFromStr parses checkpoints in the ':' format. +func newCheckpointFromStr(checkpoint string) (chaincfg.Checkpoint, error) { + parts := strings.Split(checkpoint, ":") + if len(parts) != 2 { + return chaincfg.Checkpoint{}, errors.New("checkpoints must use the " + + "syntax ':'") + } + + height, err := strconv.ParseInt(parts[0], 10, 32) + if err != nil { + return chaincfg.Checkpoint{}, fmt.Errorf("unable to parse checkpoint "+ + "due to malformed height: %s", parts[0]) + } + + hash, err := chainhash.NewHashFromStr(parts[1]) + if err != nil { + return chaincfg.Checkpoint{}, fmt.Errorf("unable to parse checkpoint "+ + "due to malformed hash: %s", parts[1]) + } + + return chaincfg.Checkpoint{ + Height: int32(height), + Hash: hash, + }, nil +} + +// parseCheckpoints checks the checkpoint strings for valid syntax +// (':') and parses them to chaincfg.Checkpoint instances. +func parseCheckpoints(checkpointStrings []string) ([]chaincfg.Checkpoint, error) { + if len(checkpointStrings) == 0 { + return nil, nil + } + checkpoints := make([]chaincfg.Checkpoint, len(checkpointStrings)) + for i, cpString := range checkpointStrings { + checkpoint, err := newCheckpointFromStr(cpString) + if err != nil { + return nil, err + } + checkpoints[i] = checkpoint + } + return checkpoints, nil +} + // filesExists reports whether the named file or directory exists. func fileExists(name string) bool { if _, err := os.Stat(name); err != nil { @@ -804,6 +851,16 @@ func loadConfig() (*config, []string, error) { return nil, nil, err } + // Check the checkpoints for syntax errors. + cfg.addCheckpoints, err = parseCheckpoints(cfg.AddCheckpoints) + if err != nil { + str := "%s: Error parsing checkpoints: %v" + err := fmt.Errorf(str, funcName, err) + fmt.Fprintln(os.Stderr, err) + fmt.Fprintln(os.Stderr, usageMessage) + return nil, nil, err + } + // Tor stream isolation requires either proxy or onion proxy to be set. if cfg.TorIsolation && cfg.Proxy == "" && cfg.OnionProxy == "" { str := "%s: Tor stream isolation requires either proxy or " + diff --git a/doc.go b/doc.go index 07fff9e4..2d65d4c8 100644 --- a/doc.go +++ b/doc.go @@ -74,6 +74,8 @@ Application Options: --testnet Use the test network --regtest Use the regression test network --simnet Use the simulation test network + --addcheckpoint= Add ad additional checkpoint. + Format: ':' --nocheckpoints Disable built-in checkpoints. Don't do this unless you know what you're doing. --dbtype= Database backend to use for the Block Chain (ffldb) diff --git a/sample-btcd.conf b/sample-btcd.conf index 71a5c928..8345d799 100644 --- a/sample-btcd.conf +++ b/sample-btcd.conf @@ -153,6 +153,9 @@ ; Disable peer bloom filtering. See BIP0111. ; nopeerbloomfilters=1 +; Add additional checkpoints. Format: ':' +; addcheckpoint=: + ; ------------------------------------------------------------------------------ ; RPC server options - The following options control the built-in RPC server