github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/follower/consensus_follower.go (about)

     1  package follower
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  
     8  	"github.com/dgraph-io/badger/v2"
     9  	"github.com/onflow/crypto"
    10  	"github.com/rs/zerolog"
    11  
    12  	"github.com/onflow/flow-go/cmd"
    13  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    14  	"github.com/onflow/flow-go/consensus/hotstuff/notifications/pubsub"
    15  	"github.com/onflow/flow-go/model/flow"
    16  	"github.com/onflow/flow-go/module/chainsync"
    17  	"github.com/onflow/flow-go/module/compliance"
    18  	"github.com/onflow/flow-go/module/component"
    19  	"github.com/onflow/flow-go/module/irrecoverable"
    20  	"github.com/onflow/flow-go/module/util"
    21  )
    22  
    23  // ConsensusFollower is a standalone module run by third parties which provides
    24  // a mechanism for observing the block chain. It maintains a set of subscribers
    25  // and delivers block proposals broadcasted by the consensus nodes to each one.
    26  type ConsensusFollower interface {
    27  	component.Component
    28  	// Run starts the consensus follower.
    29  	Run(context.Context)
    30  	// AddOnBlockFinalizedConsumer adds a new block finalization subscriber.
    31  	AddOnBlockFinalizedConsumer(pubsub.OnBlockFinalizedConsumer)
    32  }
    33  
    34  // Config contains the configurable fields for a `ConsensusFollower`.
    35  type Config struct {
    36  	networkPrivKey   crypto.PrivateKey   // the network private key of this node
    37  	bootstrapNodes   []BootstrapNodeInfo // the bootstrap nodes to use
    38  	bindAddr         string              // address to bind on
    39  	db               *badger.DB          // the badger DB storage to use for the protocol state
    40  	dataDir          string              // directory to store the protocol state (if the badger storage is not provided)
    41  	bootstrapDir     string              // path to the bootstrap directory
    42  	logLevel         string              // log level
    43  	exposeMetrics    bool                // whether to expose metrics
    44  	syncConfig       *chainsync.Config   // sync core configuration
    45  	complianceConfig *compliance.Config  // follower engine configuration
    46  }
    47  
    48  type Option func(c *Config)
    49  
    50  // WithDataDir sets the underlying directory to be used to store the database
    51  // If a database is supplied, then data directory will be set to empty string
    52  func WithDataDir(dataDir string) Option {
    53  	return func(cf *Config) {
    54  		if cf.db == nil {
    55  			cf.dataDir = dataDir
    56  		}
    57  	}
    58  }
    59  
    60  func WithBootstrapDir(bootstrapDir string) Option {
    61  	return func(cf *Config) {
    62  		cf.bootstrapDir = bootstrapDir
    63  	}
    64  }
    65  
    66  func WithLogLevel(level string) Option {
    67  	return func(cf *Config) {
    68  		cf.logLevel = level
    69  	}
    70  }
    71  
    72  // WithDB sets the underlying database that will be used to store the chain state
    73  // WithDB takes precedence over WithDataDir and datadir will be set to empty if DB is set using this option
    74  func WithDB(db *badger.DB) Option {
    75  	return func(cf *Config) {
    76  		cf.db = db
    77  		cf.dataDir = ""
    78  	}
    79  }
    80  
    81  func WithExposeMetrics(expose bool) Option {
    82  	return func(c *Config) {
    83  		c.exposeMetrics = expose
    84  	}
    85  }
    86  
    87  func WithSyncCoreConfig(config *chainsync.Config) Option {
    88  	return func(c *Config) {
    89  		c.syncConfig = config
    90  	}
    91  }
    92  
    93  func WithComplianceConfig(config *compliance.Config) Option {
    94  	return func(c *Config) {
    95  		c.complianceConfig = config
    96  	}
    97  }
    98  
    99  // BootstrapNodeInfo contains the details about the upstream bootstrap peer the consensus follower uses
   100  type BootstrapNodeInfo struct {
   101  	Host             string // ip or hostname
   102  	Port             uint
   103  	NetworkPublicKey crypto.PublicKey // the network public key of the bootstrap peer
   104  }
   105  
   106  func bootstrapIdentities(bootstrapNodes []BootstrapNodeInfo) flow.IdentitySkeletonList {
   107  	ids := make(flow.IdentitySkeletonList, len(bootstrapNodes))
   108  	for i, b := range bootstrapNodes {
   109  		ids[i] = &flow.IdentitySkeleton{
   110  			Role:          flow.RoleAccess,
   111  			NetworkPubKey: b.NetworkPublicKey,
   112  			Address:       fmt.Sprintf("%s:%d", b.Host, b.Port),
   113  			StakingPubKey: nil,
   114  		}
   115  	}
   116  	return ids
   117  }
   118  
   119  func getFollowerServiceOptions(config *Config) []FollowerOption {
   120  	ids := bootstrapIdentities(config.bootstrapNodes)
   121  	return []FollowerOption{
   122  		WithBootStrapPeers(ids...),
   123  		WithBaseOptions(getBaseOptions(config)),
   124  		WithNetworkKey(config.networkPrivKey),
   125  	}
   126  }
   127  
   128  func getBaseOptions(config *Config) []cmd.Option {
   129  	options := []cmd.Option{
   130  		cmd.WithMetricsEnabled(false),
   131  		cmd.WithSecretsDBEnabled(false),
   132  	}
   133  	if config.bootstrapDir != "" {
   134  		options = append(options, cmd.WithBootstrapDir(config.bootstrapDir))
   135  	}
   136  	if config.dataDir != "" {
   137  		options = append(options, cmd.WithDataDir(config.dataDir))
   138  	}
   139  	if config.bindAddr != "" {
   140  		options = append(options, cmd.WithBindAddress(config.bindAddr))
   141  	}
   142  	if config.logLevel != "" {
   143  		options = append(options, cmd.WithLogLevel(config.logLevel))
   144  	}
   145  	if config.db != nil {
   146  		options = append(options, cmd.WithDB(config.db))
   147  	}
   148  	if config.exposeMetrics {
   149  		options = append(options, cmd.WithMetricsEnabled(config.exposeMetrics))
   150  	}
   151  	if config.syncConfig != nil {
   152  		options = append(options, cmd.WithSyncCoreConfig(*config.syncConfig))
   153  	}
   154  	if config.complianceConfig != nil {
   155  		options = append(options, cmd.WithComplianceConfig(*config.complianceConfig))
   156  	}
   157  
   158  	return options
   159  }
   160  
   161  func buildConsensusFollower(opts []FollowerOption) (*FollowerServiceBuilder, error) {
   162  	nodeBuilder := FlowConsensusFollowerService(opts...)
   163  
   164  	if err := nodeBuilder.Initialize(); err != nil {
   165  		return nil, err
   166  	}
   167  
   168  	return nodeBuilder, nil
   169  }
   170  
   171  type ConsensusFollowerImpl struct {
   172  	component.Component
   173  	*cmd.NodeConfig
   174  	logger      zerolog.Logger
   175  	consumersMu sync.RWMutex
   176  	consumers   []pubsub.OnBlockFinalizedConsumer
   177  }
   178  
   179  // NewConsensusFollower creates a new consensus follower.
   180  func NewConsensusFollower(
   181  	networkPrivKey crypto.PrivateKey,
   182  	bindAddr string,
   183  	bootstapIdentities []BootstrapNodeInfo,
   184  	opts ...Option,
   185  ) (*ConsensusFollowerImpl, error) {
   186  	config := &Config{
   187  		networkPrivKey: networkPrivKey,
   188  		bootstrapNodes: bootstapIdentities,
   189  		bindAddr:       bindAddr,
   190  		logLevel:       "info",
   191  		exposeMetrics:  false,
   192  	}
   193  
   194  	for _, opt := range opts {
   195  		opt(config)
   196  	}
   197  
   198  	accessNodeOptions := getFollowerServiceOptions(config)
   199  	anb, err := buildConsensusFollower(accessNodeOptions)
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  
   204  	cf := &ConsensusFollowerImpl{logger: anb.Logger}
   205  	anb.BaseConfig.NodeRole = "consensus_follower"
   206  	anb.FollowerDistributor.AddOnBlockFinalizedConsumer(cf.onBlockFinalized)
   207  	cf.NodeConfig = anb.NodeConfig
   208  
   209  	cf.Component, err = anb.Build()
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  
   214  	return cf, nil
   215  }
   216  
   217  // onBlockFinalized relays the block finalization event to all registered consumers.
   218  func (cf *ConsensusFollowerImpl) onBlockFinalized(finalizedBlock *model.Block) {
   219  	cf.consumersMu.RLock()
   220  	for _, consumer := range cf.consumers {
   221  		cf.consumersMu.RUnlock()
   222  		consumer(finalizedBlock)
   223  		cf.consumersMu.RLock()
   224  	}
   225  	cf.consumersMu.RUnlock()
   226  }
   227  
   228  // AddOnBlockFinalizedConsumer adds a new block finalization subscriber.
   229  func (cf *ConsensusFollowerImpl) AddOnBlockFinalizedConsumer(consumer pubsub.OnBlockFinalizedConsumer) {
   230  	cf.consumersMu.Lock()
   231  	defer cf.consumersMu.Unlock()
   232  	cf.consumers = append(cf.consumers, consumer)
   233  }
   234  
   235  // Run starts the consensus follower.
   236  // This may also be implemented directly in a calling library to take advantage of error recovery
   237  // possible with the irrecoverable error handling.
   238  func (cf *ConsensusFollowerImpl) Run(ctx context.Context) {
   239  	if util.CheckClosed(ctx.Done()) {
   240  		return
   241  	}
   242  
   243  	// Start the consensus follower with an irrecoverable signaler context. The returned error channel
   244  	// will receive irrecoverable errors thrown by the consensus follower or any of its child components.
   245  	// This makes it possible to listen for irrecoverable errors and restart the consensus follower. In
   246  	// the default implementation, a fatal error is thrown.
   247  	signalerCtx, errChan := irrecoverable.WithSignaler(ctx)
   248  	cf.Start(signalerCtx)
   249  
   250  	// log when the follower has complete startup and when it's beginning to shut down
   251  	go func() {
   252  		if err := util.WaitClosed(ctx, cf.Ready()); err != nil {
   253  			return
   254  		}
   255  		cf.logger.Info().Msg("Consensus follower startup complete")
   256  	}()
   257  
   258  	go func() {
   259  		<-ctx.Done()
   260  		cf.logger.Info().Msg("Consensus follower shutting down")
   261  	}()
   262  
   263  	// Block here until all components have stopped or an irrecoverable error is received.
   264  	if err := util.WaitError(errChan, cf.Done()); err != nil {
   265  		cf.logger.Fatal().Err(err).Msg("A fatal error was encountered in consensus follower")
   266  	}
   267  	cf.logger.Info().Msg("Consensus follower shutdown complete")
   268  }