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 }