github.com/hechain20/hechain@v0.0.0-20220316014945-b544036ba106/orderer/common/follower/follower_chain.go (about)

     1  /*
     2  Copyright hechain. 2017 All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package follower
     8  
     9  import (
    10  	"bytes"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/hechain20/hechain/bccsp"
    15  	"github.com/hechain20/hechain/common/flogging"
    16  	"github.com/hechain20/hechain/orderer/common/cluster"
    17  	"github.com/hechain20/hechain/orderer/common/types"
    18  	"github.com/hechain20/hechain/orderer/consensus"
    19  	"github.com/hechain20/hechain/protoutil"
    20  	"github.com/hyperledger/fabric-protos-go/common"
    21  	"github.com/pkg/errors"
    22  )
    23  
    24  // ErrChainStopped is returned when the chain is stopped during execution.
    25  var ErrChainStopped = errors.New("chain stopped")
    26  
    27  //go:generate counterfeiter -o mocks/ledger_resources.go -fake-name LedgerResources . LedgerResources
    28  
    29  // LedgerResources defines some of the interfaces of ledger & config resources needed by the follower.Chain.
    30  type LedgerResources interface {
    31  	// ChannelID The channel ID.
    32  	ChannelID() string
    33  
    34  	// Block returns a block with the given number,
    35  	// or nil if such a block doesn't exist.
    36  	Block(number uint64) *common.Block
    37  
    38  	// Height returns the number of blocks in the chain this channel is associated with.
    39  	Height() uint64
    40  
    41  	// Append appends a new block to the ledger in its raw form.
    42  	Append(block *common.Block) error
    43  }
    44  
    45  // TimeAfter has the signature of time.After and allows tests to provide an alternative implementation to it.
    46  type TimeAfter func(d time.Duration) <-chan time.Time
    47  
    48  //go:generate counterfeiter -o mocks/block_puller_factory.go -fake-name BlockPullerFactory . BlockPullerFactory
    49  
    50  // BlockPullerFactory creates a ChannelPuller on demand, and exposes a method to update the a block signature verifier
    51  // linked to that ChannelPuller.
    52  type BlockPullerFactory interface {
    53  	BlockPuller(configBlock *common.Block, stopChannel chan struct{}) (ChannelPuller, error)
    54  	UpdateVerifierFromConfigBlock(configBlock *common.Block) error
    55  }
    56  
    57  //go:generate counterfeiter -o mocks/chain_creator.go -fake-name ChainCreator . ChainCreator
    58  
    59  // ChainCreator defines a function that creates a new consensus.Chain for this channel, to replace the current
    60  // follower.Chain. This interface is meant to be implemented by the multichannel.Registrar.
    61  type ChainCreator interface {
    62  	SwitchFollowerToChain(chainName string)
    63  }
    64  
    65  //go:generate counterfeiter -o mocks/channel_participation_metrics_reporter.go -fake-name ChannelParticipationMetricsReporter . ChannelParticipationMetricsReporter
    66  
    67  type ChannelParticipationMetricsReporter interface {
    68  	ReportConsensusRelationAndStatusMetrics(channelID string, relation types.ConsensusRelation, status types.Status)
    69  }
    70  
    71  // Chain implements a component that allows the orderer to follow a specific channel when is not a cluster member,
    72  // that is, be a "follower" of the cluster. It also allows the orderer to perform "onboarding" for
    73  // channels it is joining as a member, with a join-block.
    74  //
    75  // When an orderer is following a channel, it means that the current orderer is not a member of the consenters set
    76  // of the channel, and is only pulling blocks from other orderers. In this mode, the follower is inspecting config
    77  // blocks as they are pulled and if it discovers that it was introduced into the consenters set, it will trigger the
    78  // creation of a regular etcdraft.Chain, that is, turn into a "member" of the cluster.
    79  //
    80  // A follower is also used to onboard a channel when joining as a member with a join-block that has number >0. In this
    81  // mode the follower will pull blocks up until join-block.number, and then will trigger the creation of a regular
    82  // etcdraft.Chain.
    83  //
    84  // The follower is started in one of two ways: 1) following an API Join request with a join-block that has
    85  // block number >0, or 2) when the orderer was a cluster member (i.e. was running a etcdraft.Chain) and was removed
    86  // from the consenters set.
    87  //
    88  // The follower is in status "onboarding" when it pulls blocks below the join-block number, or "active" when it
    89  // pulls blocks equal or above the join-block number.
    90  //
    91  // The follower return clusterRelation "member" when the join-block indicates the orderer is in the consenters set,
    92  // i.e. the follower is performing onboarding for an etcdraft.Chain. Otherwise, the follower return clusterRelation
    93  // "follower".
    94  type Chain struct {
    95  	mutex             sync.Mutex    // Protects the start/stop flags & channels, consensusRelation & status. All the rest are immutable or accessed only by the go-routine.
    96  	started           bool          // Start once.
    97  	stopped           bool          // Stop once.
    98  	stopChan          chan struct{} // A 'closer' signals the go-routine to stop by closing this channel.
    99  	doneChan          chan struct{} // The go-routine signals the 'closer' that it is done by closing this channel.
   100  	consensusRelation types.ConsensusRelation
   101  	status            types.Status
   102  
   103  	ledgerResources  LedgerResources            // ledger & config resources
   104  	clusterConsenter consensus.ClusterConsenter // detects whether a block indicates channel membership
   105  	options          Options
   106  	logger           *flogging.FabricLogger
   107  	timeAfter        TimeAfter // time.After by default, or an alternative from Options.
   108  
   109  	joinBlock   *common.Block // The join-block the follower was started with.
   110  	lastConfig  *common.Block // The last config block from the ledger. Accessed only by the go-routine.
   111  	firstHeight uint64        // The first ledger height
   112  
   113  	// Creates a block puller on demand, and allows the update of the block signature verifier with each incoming
   114  	// config block.
   115  	blockPullerFactory BlockPullerFactory
   116  	// A block puller instance, created either from the join-block or last-config-block. When pulling blocks using
   117  	// the last-config-block, the endpoints are updated with each incoming config block.
   118  	blockPuller ChannelPuller
   119  
   120  	// Creates a new consensus.Chain for this channel, to replace the current follower.Chain.
   121  	chainCreator ChainCreator
   122  
   123  	cryptoProvider bccsp.BCCSP // Cryptographic services
   124  
   125  	channelParticipationMetricsReporter ChannelParticipationMetricsReporter
   126  }
   127  
   128  // NewChain constructs a follower.Chain object.
   129  func NewChain(
   130  	ledgerResources LedgerResources,
   131  	clusterConsenter consensus.ClusterConsenter,
   132  	joinBlock *common.Block,
   133  	options Options,
   134  	blockPullerFactory BlockPullerFactory,
   135  	chainCreator ChainCreator,
   136  	cryptoProvider bccsp.BCCSP,
   137  	channelParticipationMetricsReporter ChannelParticipationMetricsReporter,
   138  ) (*Chain, error) {
   139  	options.applyDefaults()
   140  
   141  	chain := &Chain{
   142  		stopChan:                            make(chan struct{}),
   143  		doneChan:                            make(chan struct{}),
   144  		consensusRelation:                   types.ConsensusRelationFollower,
   145  		status:                              types.StatusOnBoarding,
   146  		ledgerResources:                     ledgerResources,
   147  		clusterConsenter:                    clusterConsenter,
   148  		joinBlock:                           joinBlock,
   149  		firstHeight:                         ledgerResources.Height(),
   150  		options:                             options,
   151  		logger:                              options.Logger.With("channel", ledgerResources.ChannelID()),
   152  		timeAfter:                           options.TimeAfter,
   153  		blockPullerFactory:                  blockPullerFactory,
   154  		chainCreator:                        chainCreator,
   155  		cryptoProvider:                      cryptoProvider,
   156  		channelParticipationMetricsReporter: channelParticipationMetricsReporter,
   157  	}
   158  
   159  	if ledgerResources.Height() > 0 {
   160  		if err := chain.loadLastConfig(); err != nil {
   161  			return nil, err
   162  		}
   163  		if err := blockPullerFactory.UpdateVerifierFromConfigBlock(chain.lastConfig); err != nil {
   164  			return nil, err
   165  		}
   166  	}
   167  
   168  	if joinBlock == nil {
   169  		chain.status = types.StatusActive
   170  		if isMem, _ := chain.clusterConsenter.IsChannelMember(chain.lastConfig); isMem {
   171  			chain.consensusRelation = types.ConsensusRelationConsenter
   172  		}
   173  
   174  		chain.logger.Infof("Created with a nil join-block, ledger height: %d", chain.firstHeight)
   175  	} else {
   176  		if joinBlock.Header == nil {
   177  			return nil, errors.New("block header is nil")
   178  		}
   179  		if joinBlock.Data == nil {
   180  			return nil, errors.New("block data is nil")
   181  		}
   182  
   183  		// Check the block puller creation function once before we start the follower. This ensures we can extract
   184  		// the endpoints from the join-block.
   185  		puller, err := blockPullerFactory.BlockPuller(joinBlock, nil)
   186  		if err != nil {
   187  			return nil, errors.WithMessage(err, "error creating a block puller from join-block")
   188  		}
   189  		puller.Close()
   190  
   191  		if chain.joinBlock.Header.Number < chain.ledgerResources.Height() {
   192  			chain.status = types.StatusActive
   193  		}
   194  		if isMem, _ := chain.clusterConsenter.IsChannelMember(chain.joinBlock); isMem {
   195  			chain.consensusRelation = types.ConsensusRelationConsenter
   196  		}
   197  
   198  		chain.logger.Infof("Created with join-block number: %d, ledger height: %d", joinBlock.Header.Number, chain.firstHeight)
   199  	}
   200  
   201  	chain.logger.Debugf("Options are: %v", chain.options)
   202  
   203  	chain.channelParticipationMetricsReporter.ReportConsensusRelationAndStatusMetrics(ledgerResources.ChannelID(), chain.consensusRelation, chain.status)
   204  
   205  	return chain, nil
   206  }
   207  
   208  func (c *Chain) Start() {
   209  	c.mutex.Lock()
   210  	defer c.mutex.Unlock()
   211  
   212  	if c.started || c.stopped {
   213  		c.logger.Debugf("Not starting because: started=%v, stopped=%v", c.started, c.stopped)
   214  		return
   215  	}
   216  
   217  	c.started = true
   218  
   219  	go c.run()
   220  
   221  	c.logger.Info("Started")
   222  }
   223  
   224  // Halt signals the Chain to stop and waits for the internal go-routine to exit.
   225  func (c *Chain) Halt() {
   226  	c.halt()
   227  	<-c.doneChan
   228  }
   229  
   230  func (c *Chain) halt() {
   231  	c.mutex.Lock()
   232  	defer c.mutex.Unlock()
   233  
   234  	if c.stopped {
   235  		c.logger.Debug("Already stopped")
   236  		return
   237  	}
   238  	c.stopped = true
   239  	close(c.stopChan)
   240  	c.logger.Info("Stopped")
   241  }
   242  
   243  // StatusReport returns the ConsensusRelation & Status.
   244  func (c *Chain) StatusReport() (types.ConsensusRelation, types.Status) {
   245  	c.mutex.Lock()
   246  	defer c.mutex.Unlock()
   247  
   248  	return c.consensusRelation, c.status
   249  }
   250  
   251  func (c *Chain) setStatus(status types.Status) {
   252  	c.mutex.Lock()
   253  	defer c.mutex.Unlock()
   254  
   255  	c.status = status
   256  
   257  	c.channelParticipationMetricsReporter.ReportConsensusRelationAndStatusMetrics(c.ledgerResources.ChannelID(), c.consensusRelation, c.status)
   258  }
   259  
   260  func (c *Chain) setConsensusRelation(clusterRelation types.ConsensusRelation) {
   261  	c.mutex.Lock()
   262  	defer c.mutex.Unlock()
   263  
   264  	c.consensusRelation = clusterRelation
   265  
   266  	c.channelParticipationMetricsReporter.ReportConsensusRelationAndStatusMetrics(c.ledgerResources.ChannelID(), c.consensusRelation, c.status)
   267  }
   268  
   269  func (c *Chain) Height() uint64 {
   270  	return c.ledgerResources.Height()
   271  }
   272  
   273  func (c *Chain) IsRunning() bool {
   274  	c.mutex.Lock()
   275  	defer c.mutex.Unlock()
   276  
   277  	if c.started {
   278  		select {
   279  		case <-c.doneChan:
   280  			return false
   281  		default:
   282  			return true
   283  		}
   284  	}
   285  
   286  	return false
   287  }
   288  
   289  func (c *Chain) run() {
   290  	c.logger.Debug("The follower.Chain puller goroutine is starting")
   291  
   292  	defer func() {
   293  		close(c.doneChan)
   294  		c.logger.Debug("The follower.Chain puller goroutine is exiting")
   295  	}()
   296  
   297  	if err := c.pull(); err != nil {
   298  		c.logger.Warnf("Pull failed, error: %s", err)
   299  		// TODO set the status to StatusError (see FAB-18106)
   300  	}
   301  }
   302  
   303  func (c *Chain) increaseRetryInterval(retryInterval *time.Duration, upperLimit time.Duration) {
   304  	if *retryInterval == upperLimit {
   305  		return
   306  	}
   307  	// assuming this will never overflow int64, as upperLimit cannot be over MaxInt64/2
   308  	*retryInterval = time.Duration(1.5 * float64(*retryInterval))
   309  	if *retryInterval > upperLimit {
   310  		*retryInterval = upperLimit
   311  	}
   312  	c.logger.Debugf("retry interval increased to: %v", *retryInterval)
   313  }
   314  
   315  func (c *Chain) resetRetryInterval(retryInterval *time.Duration, lowerLimit time.Duration) {
   316  	if *retryInterval == lowerLimit {
   317  		return
   318  	}
   319  	*retryInterval = lowerLimit
   320  	c.logger.Debugf("retry interval reset to: %v", *retryInterval)
   321  }
   322  
   323  func (c *Chain) decreaseRetryInterval(retryInterval *time.Duration, lowerLimit time.Duration) {
   324  	if *retryInterval == lowerLimit {
   325  		return
   326  	}
   327  
   328  	*retryInterval = *retryInterval - lowerLimit
   329  	if *retryInterval < lowerLimit {
   330  		*retryInterval = lowerLimit
   331  	}
   332  	c.logger.Debugf("retry interval decreased to: %v", *retryInterval)
   333  }
   334  
   335  // pull blocks from other orderers until a config block indicates the orderer has become a member of the cluster.
   336  // When the follower.Chain's job is done, this method halts, triggers the creation of a new consensus.Chain,
   337  // and returns nil. The method returns an error only when the chain is stopped or due to unrecoverable errors.
   338  func (c *Chain) pull() error {
   339  	var err error
   340  	if c.joinBlock != nil {
   341  		err = c.pullUpToJoin()
   342  		if err != nil {
   343  			return errors.WithMessage(err, "failed to pull up to join block")
   344  		}
   345  		c.logger.Info("Onboarding finished successfully, pulled blocks up to join-block")
   346  	}
   347  
   348  	err = c.pullAfterJoin()
   349  	if err != nil {
   350  		return errors.WithMessage(err, "failed to pull after join block")
   351  	}
   352  
   353  	// Trigger creation of a new consensus.Chain.
   354  	c.logger.Info("Block pulling finished successfully, going to switch from follower to a consensus.Chain")
   355  	c.halt()
   356  	c.chainCreator.SwitchFollowerToChain(c.ledgerResources.ChannelID())
   357  
   358  	return nil
   359  }
   360  
   361  // pullUpToJoin pulls blocks up to the join-block height without inspecting membership on fetched config blocks.
   362  // It checks whether the chain was stopped between blocks.
   363  func (c *Chain) pullUpToJoin() error {
   364  	targetHeight := c.joinBlock.Header.Number + 1
   365  	if c.ledgerResources.Height() >= targetHeight {
   366  		c.logger.Infof("Target height according to join block (%d) is <= to our ledger height (%d), no need to pull up to join block",
   367  			targetHeight, c.ledgerResources.Height())
   368  		return nil
   369  	}
   370  
   371  	var err error
   372  	// Block puller created with endpoints from the join-block.
   373  	c.blockPuller, err = c.blockPullerFactory.BlockPuller(c.joinBlock, c.stopChan)
   374  	if err != nil { // This should never happen since we check the join-block before we start.
   375  		return errors.WithMessagef(err, "error creating block puller")
   376  	}
   377  	defer c.blockPuller.Close()
   378  	// Since we created the block-puller with the join-block, do not update the endpoints from the
   379  	// config blocks that precede it.
   380  	err = c.pullUntilLatestWithRetry(targetHeight, false)
   381  	if err != nil {
   382  		return err
   383  	}
   384  
   385  	c.logger.Infof("Pulled blocks from %d until %d", c.firstHeight, targetHeight-1)
   386  	return nil
   387  }
   388  
   389  // pullAfterJoin pulls blocks continuously, inspecting the fetched config
   390  // blocks for membership. On every config block, it renews the BlockPuller,
   391  // to take in the new configuration. It will exit with 'nil' if it detects
   392  // a config block that indicates the orderer is a member of the cluster. It
   393  // checks whether the chain was stopped between blocks.
   394  func (c *Chain) pullAfterJoin() error {
   395  	c.setStatus(types.StatusActive)
   396  
   397  	err := c.loadLastConfig()
   398  	if err != nil {
   399  		return errors.WithMessage(err, "failed to load last config block")
   400  	}
   401  
   402  	c.blockPuller, err = c.blockPullerFactory.BlockPuller(c.lastConfig, c.stopChan)
   403  	if err != nil {
   404  		return errors.WithMessage(err, "error creating block puller")
   405  	}
   406  	defer c.blockPuller.Close()
   407  
   408  	heightPollInterval := c.options.HeightPollMinInterval
   409  	for {
   410  		// Check membership
   411  		isMember, errMem := c.clusterConsenter.IsChannelMember(c.lastConfig)
   412  		if errMem != nil {
   413  			return errors.WithMessage(err, "failed to determine channel membership from last config")
   414  		}
   415  		if isMember {
   416  			c.setConsensusRelation(types.ConsensusRelationConsenter)
   417  			return nil
   418  		}
   419  
   420  		// Poll for latest network height to advance beyond ledger height.
   421  		var latestNetworkHeight uint64
   422  	heightPollLoop:
   423  		for {
   424  			endpoint, networkHeight, errHeight := cluster.LatestHeightAndEndpoint(c.blockPuller)
   425  			if errHeight != nil {
   426  				c.logger.Errorf("Failed to get latest height and endpoint, error: %s", errHeight)
   427  			} else {
   428  				c.logger.Debugf("Orderer endpoint %s has the biggest ledger height: %d", endpoint, networkHeight)
   429  			}
   430  
   431  			if networkHeight > c.ledgerResources.Height() {
   432  				// On success, slowly decrease the polling interval
   433  				c.decreaseRetryInterval(&heightPollInterval, c.options.HeightPollMinInterval)
   434  				latestNetworkHeight = networkHeight
   435  				break heightPollLoop
   436  			}
   437  
   438  			c.logger.Debugf("My height: %d, latest network height: %d; going to wait %v for latest height to grow",
   439  				c.ledgerResources.Height(), networkHeight, heightPollInterval)
   440  			select {
   441  			case <-c.stopChan:
   442  				c.logger.Debug("Received a stop signal")
   443  				return ErrChainStopped
   444  			case <-c.timeAfter(heightPollInterval):
   445  				// Exponential back-off, to avoid calling LatestHeightAndEndpoint too often.
   446  				c.increaseRetryInterval(&heightPollInterval, c.options.HeightPollMaxInterval)
   447  			}
   448  		}
   449  
   450  		// Pull to latest height or chain stop signal
   451  		err = c.pullUntilLatestWithRetry(latestNetworkHeight, true)
   452  		if err != nil {
   453  			return err
   454  		}
   455  	}
   456  }
   457  
   458  // pullUntilLatestWithRetry is given a target-height and exits without an error when it reaches that target.
   459  // It return with an error only if the chain is stopped.
   460  // On internal pull errors it employs exponential back-off and retries.
   461  // When parameter updateEndpoints is true, the block-puller's endpoints are updated with every incoming config.
   462  func (c *Chain) pullUntilLatestWithRetry(latestNetworkHeight uint64, updateEndpoints bool) error {
   463  	retryInterval := c.options.PullRetryMinInterval
   464  	for {
   465  		numPulled, errPull := c.pullUntilTarget(latestNetworkHeight, updateEndpoints)
   466  		if numPulled > 0 {
   467  			c.resetRetryInterval(&retryInterval, c.options.PullRetryMinInterval) // On any progress, reset retry interval.
   468  		}
   469  		if errPull == nil {
   470  			c.logger.Debugf("Pulled %d blocks until latest network height: %d", numPulled, latestNetworkHeight)
   471  			break
   472  		}
   473  
   474  		c.logger.Debugf("Error while trying to pull to latest height: %d; going to try again in %v",
   475  			latestNetworkHeight, retryInterval)
   476  		select {
   477  		case <-c.stopChan:
   478  			c.logger.Debug("Received a stop signal")
   479  			return ErrChainStopped
   480  		case <-c.timeAfter(retryInterval):
   481  			// Exponential back-off on successive errors w/o progress.
   482  			c.increaseRetryInterval(&retryInterval, c.options.PullRetryMaxInterval)
   483  		}
   484  	}
   485  
   486  	return nil
   487  }
   488  
   489  // pullUntilTarget is given a target-height and exits without an error when it reaches that target.
   490  // It may return with an error before the target, always returning the number of blocks pulled.
   491  // When parameter updateEndpoints is true, the block-puller's endpoints are updated with every incoming config.
   492  // The block-puller-factory which holds the block signature verifier is updated on every incoming config.
   493  func (c *Chain) pullUntilTarget(targetHeight uint64, updateEndpoints bool) (uint64, error) {
   494  	firstBlockToPull := c.ledgerResources.Height()
   495  	if firstBlockToPull >= targetHeight {
   496  		c.logger.Debugf("Target height (%d) is <= to our ledger height (%d), skipping pulling", targetHeight, firstBlockToPull)
   497  		return 0, nil
   498  	}
   499  
   500  	var actualPrevHash []byte
   501  	// Initialize the actual previous hash
   502  	if firstBlockToPull > 0 {
   503  		prevBlock := c.ledgerResources.Block(firstBlockToPull - 1)
   504  		if prevBlock == nil {
   505  			return 0, errors.Errorf("cannot retrieve previous block %d", firstBlockToPull-1)
   506  		}
   507  		actualPrevHash = protoutil.BlockHeaderHash(prevBlock.Header)
   508  	}
   509  
   510  	// Pull until the latest height
   511  	for seq := firstBlockToPull; seq < targetHeight; seq++ {
   512  		n := seq - firstBlockToPull
   513  		select {
   514  		case <-c.stopChan:
   515  			c.logger.Debug("Received a stop signal")
   516  			return n, ErrChainStopped
   517  		default:
   518  			nextBlock := c.blockPuller.PullBlock(seq)
   519  			if nextBlock == nil {
   520  				return n, errors.WithMessagef(cluster.ErrRetryCountExhausted, "failed to pull block %d", seq)
   521  			}
   522  			reportedPrevHash := nextBlock.Header.PreviousHash
   523  			if (nextBlock.Header.Number > 0) && !bytes.Equal(reportedPrevHash, actualPrevHash) {
   524  				return n, errors.Errorf("block header mismatch on sequence %d, expected %x, got %x",
   525  					nextBlock.Header.Number, actualPrevHash, reportedPrevHash)
   526  			}
   527  			actualPrevHash = protoutil.BlockHeaderHash(nextBlock.Header)
   528  			if err := c.ledgerResources.Append(nextBlock); err != nil {
   529  				return n, errors.WithMessagef(err, "failed to append block %d to the ledger", nextBlock.Header.Number)
   530  			}
   531  
   532  			if protoutil.IsConfigBlock(nextBlock) {
   533  				c.logger.Debugf("Pulled blocks from %d to %d, last block is config", firstBlockToPull, nextBlock.Header.Number)
   534  				c.lastConfig = nextBlock
   535  				if err := c.blockPullerFactory.UpdateVerifierFromConfigBlock(nextBlock); err != nil {
   536  					return n, errors.WithMessagef(err, "failed to update verifier from last config,  block number: %d", nextBlock.Header.Number)
   537  				}
   538  				if updateEndpoints {
   539  					endpoints, err := cluster.EndpointconfigFromConfigBlock(nextBlock, c.cryptoProvider)
   540  					if err != nil {
   541  						return n, errors.WithMessagef(err, "failed to extract endpoints from last config,  block number: %d", nextBlock.Header.Number)
   542  					}
   543  					c.blockPuller.UpdateEndpoints(endpoints)
   544  				}
   545  			}
   546  		}
   547  	}
   548  	c.logger.Debugf("Pulled blocks from %d to %d", firstBlockToPull, targetHeight)
   549  	return targetHeight - firstBlockToPull, nil
   550  }
   551  
   552  func (c *Chain) loadLastConfig() error {
   553  	height := c.ledgerResources.Height()
   554  	if height == 0 {
   555  		return errors.New("ledger is empty")
   556  	}
   557  	lastBlock := c.ledgerResources.Block(height - 1)
   558  	index, err := protoutil.GetLastConfigIndexFromBlock(lastBlock)
   559  	if err != nil {
   560  		return errors.WithMessage(err, "chain does have appropriately encoded last config in its latest block")
   561  	}
   562  	lastConfig := c.ledgerResources.Block(index)
   563  	if lastConfig == nil {
   564  		return errors.Errorf("could not retrieve config block from index %d", index)
   565  	}
   566  	c.lastConfig = lastConfig
   567  	return nil
   568  }