github.com/onflow/flow-go@v0.33.17/cmd/consensus/main.go (about)

     1  // (c) 2019 Dapper Labs - ALL RIGHTS RESERVED
     2  
     3  package main
     4  
     5  import (
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"time"
    12  
    13  	"github.com/spf13/pflag"
    14  
    15  	client "github.com/onflow/flow-go-sdk/access/grpc"
    16  	"github.com/onflow/flow-go-sdk/crypto"
    17  	"github.com/onflow/flow-go/cmd"
    18  	"github.com/onflow/flow-go/cmd/util/cmd/common"
    19  	"github.com/onflow/flow-go/consensus"
    20  	"github.com/onflow/flow-go/consensus/hotstuff"
    21  	"github.com/onflow/flow-go/consensus/hotstuff/blockproducer"
    22  	"github.com/onflow/flow-go/consensus/hotstuff/committees"
    23  	"github.com/onflow/flow-go/consensus/hotstuff/cruisectl"
    24  	"github.com/onflow/flow-go/consensus/hotstuff/notifications"
    25  	"github.com/onflow/flow-go/consensus/hotstuff/notifications/pubsub"
    26  	"github.com/onflow/flow-go/consensus/hotstuff/pacemaker/timeout"
    27  	"github.com/onflow/flow-go/consensus/hotstuff/persister"
    28  	hotsignature "github.com/onflow/flow-go/consensus/hotstuff/signature"
    29  	"github.com/onflow/flow-go/consensus/hotstuff/timeoutcollector"
    30  	"github.com/onflow/flow-go/consensus/hotstuff/verification"
    31  	"github.com/onflow/flow-go/consensus/hotstuff/votecollector"
    32  	recovery "github.com/onflow/flow-go/consensus/recovery/protocol"
    33  	"github.com/onflow/flow-go/engine/common/requester"
    34  	synceng "github.com/onflow/flow-go/engine/common/synchronization"
    35  	"github.com/onflow/flow-go/engine/consensus/approvals/tracker"
    36  	"github.com/onflow/flow-go/engine/consensus/compliance"
    37  	dkgeng "github.com/onflow/flow-go/engine/consensus/dkg"
    38  	"github.com/onflow/flow-go/engine/consensus/ingestion"
    39  	"github.com/onflow/flow-go/engine/consensus/matching"
    40  	"github.com/onflow/flow-go/engine/consensus/message_hub"
    41  	"github.com/onflow/flow-go/engine/consensus/sealing"
    42  	"github.com/onflow/flow-go/fvm/systemcontracts"
    43  	"github.com/onflow/flow-go/model/bootstrap"
    44  	"github.com/onflow/flow-go/model/encodable"
    45  	"github.com/onflow/flow-go/model/flow"
    46  	"github.com/onflow/flow-go/model/flow/filter"
    47  	"github.com/onflow/flow-go/module"
    48  	"github.com/onflow/flow-go/module/buffer"
    49  	builder "github.com/onflow/flow-go/module/builder/consensus"
    50  	"github.com/onflow/flow-go/module/chainsync"
    51  	chmodule "github.com/onflow/flow-go/module/chunks"
    52  	dkgmodule "github.com/onflow/flow-go/module/dkg"
    53  	"github.com/onflow/flow-go/module/epochs"
    54  	finalizer "github.com/onflow/flow-go/module/finalizer/consensus"
    55  	"github.com/onflow/flow-go/module/mempool"
    56  	consensusMempools "github.com/onflow/flow-go/module/mempool/consensus"
    57  	"github.com/onflow/flow-go/module/mempool/stdmap"
    58  	"github.com/onflow/flow-go/module/metrics"
    59  	msig "github.com/onflow/flow-go/module/signature"
    60  	"github.com/onflow/flow-go/module/updatable_configs"
    61  	"github.com/onflow/flow-go/module/util"
    62  	"github.com/onflow/flow-go/module/validation"
    63  	"github.com/onflow/flow-go/network/channels"
    64  	"github.com/onflow/flow-go/state/protocol"
    65  	badgerState "github.com/onflow/flow-go/state/protocol/badger"
    66  	"github.com/onflow/flow-go/state/protocol/blocktimer"
    67  	"github.com/onflow/flow-go/state/protocol/events/gadgets"
    68  	"github.com/onflow/flow-go/storage"
    69  	bstorage "github.com/onflow/flow-go/storage/badger"
    70  	"github.com/onflow/flow-go/utils/io"
    71  )
    72  
    73  func main() {
    74  
    75  	var (
    76  		guaranteeLimit                        uint
    77  		resultLimit                           uint
    78  		approvalLimit                         uint
    79  		sealLimit                             uint
    80  		pendingReceiptsLimit                  uint
    81  		minInterval                           time.Duration
    82  		maxInterval                           time.Duration
    83  		maxSealPerBlock                       uint
    84  		maxGuaranteePerBlock                  uint
    85  		hotstuffMinTimeout                    time.Duration
    86  		hotstuffTimeoutAdjustmentFactor       float64
    87  		hotstuffHappyPathMaxRoundFailures     uint64
    88  		chunkAlpha                            uint
    89  		requiredApprovalsForSealVerification  uint
    90  		requiredApprovalsForSealConstruction  uint
    91  		emergencySealing                      bool
    92  		dkgMessagingEngineConfig              = dkgeng.DefaultMessagingEngineConfig()
    93  		cruiseCtlConfig                       = cruisectl.DefaultConfig()
    94  		cruiseCtlTargetTransitionTimeFlag     = cruiseCtlConfig.TargetTransition.String()
    95  		cruiseCtlFallbackProposalDurationFlag time.Duration
    96  		cruiseCtlMinViewDurationFlag          time.Duration
    97  		cruiseCtlMaxViewDurationFlag          time.Duration
    98  		cruiseCtlEnabledFlag                  bool
    99  		startupTimeString                     string
   100  		startupTime                           time.Time
   101  
   102  		// DKG contract client
   103  		machineAccountInfo *bootstrap.NodeMachineAccountInfo
   104  		flowClientConfigs  []*common.FlowClientConfig
   105  		insecureAccessAPI  bool
   106  		accessNodeIDS      []string
   107  
   108  		err                 error
   109  		mutableState        protocol.ParticipantState
   110  		beaconPrivateKey    *encodable.RandomBeaconPrivKey
   111  		guarantees          mempool.Guarantees
   112  		receipts            mempool.ExecutionTree
   113  		seals               mempool.IncorporatedResultSeals
   114  		pendingReceipts     mempool.PendingReceipts
   115  		receiptRequester    *requester.Engine
   116  		syncCore            *chainsync.Core
   117  		comp                *compliance.Engine
   118  		hot                 module.HotStuff
   119  		conMetrics          module.ConsensusMetrics
   120  		mainMetrics         module.HotstuffMetrics
   121  		receiptValidator    module.ReceiptValidator
   122  		chunkAssigner       *chmodule.ChunkAssigner
   123  		followerDistributor *pubsub.FollowerDistributor
   124  		dkgBrokerTunnel     *dkgmodule.BrokerTunnel
   125  		blockTimer          protocol.BlockTimer
   126  		proposalDurProvider hotstuff.ProposalDurationProvider
   127  		committee           *committees.Consensus
   128  		epochLookup         *epochs.EpochLookup
   129  		hotstuffModules     *consensus.HotstuffModules
   130  		dkgState            *bstorage.DKGState
   131  		safeBeaconKeys      *bstorage.SafeBeaconPrivateKeys
   132  		getSealingConfigs   module.SealingConfigsGetter
   133  	)
   134  	var deprecatedFlagBlockRateDelay time.Duration
   135  
   136  	nodeBuilder := cmd.FlowNode(flow.RoleConsensus.String())
   137  	nodeBuilder.ExtraFlags(func(flags *pflag.FlagSet) {
   138  		flags.UintVar(&guaranteeLimit, "guarantee-limit", 1000, "maximum number of guarantees in the memory pool")
   139  		flags.UintVar(&resultLimit, "result-limit", 10000, "maximum number of execution results in the memory pool")
   140  		flags.UintVar(&approvalLimit, "approval-limit", 1000, "maximum number of result approvals in the memory pool")
   141  		// the default value is able to buffer as many seals as would be generated over ~12 hours. In case it
   142  		// ever gets full, the node will simply crash instead of employing complex ejection logic.
   143  		flags.UintVar(&sealLimit, "seal-limit", 44200, "maximum number of block seals in the memory pool")
   144  		flags.UintVar(&pendingReceiptsLimit, "pending-receipts-limit", 10000, "maximum number of pending receipts in the mempool")
   145  		flags.DurationVar(&minInterval, "min-interval", time.Millisecond, "the minimum amount of time between two blocks")
   146  		flags.DurationVar(&maxInterval, "max-interval", 90*time.Second, "the maximum amount of time between two blocks")
   147  		flags.UintVar(&maxSealPerBlock, "max-seal-per-block", 100, "the maximum number of seals to be included in a block")
   148  		flags.UintVar(&maxGuaranteePerBlock, "max-guarantee-per-block", 100, "the maximum number of collection guarantees to be included in a block")
   149  		flags.DurationVar(&hotstuffMinTimeout, "hotstuff-min-timeout", 2500*time.Millisecond, "the lower timeout bound for the hotstuff pacemaker, this is also used as initial timeout")
   150  		flags.Float64Var(&hotstuffTimeoutAdjustmentFactor, "hotstuff-timeout-adjustment-factor", timeout.DefaultConfig.TimeoutAdjustmentFactor, "adjustment of timeout duration in case of time out event")
   151  		flags.Uint64Var(&hotstuffHappyPathMaxRoundFailures, "hotstuff-happy-path-max-round-failures", timeout.DefaultConfig.HappyPathMaxRoundFailures, "number of failed rounds before first timeout increase")
   152  		flags.StringVar(&cruiseCtlTargetTransitionTimeFlag, "cruise-ctl-target-epoch-transition-time", cruiseCtlTargetTransitionTimeFlag, "the target epoch switchover schedule")
   153  		flags.DurationVar(&cruiseCtlFallbackProposalDurationFlag, "cruise-ctl-fallback-proposal-duration", cruiseCtlConfig.FallbackProposalDelay.Load(), "the proposal duration value to use when the controller is disabled, or in epoch fallback mode. In those modes, this value has the same as the old `--block-rate-delay`")
   154  		flags.DurationVar(&cruiseCtlMinViewDurationFlag, "cruise-ctl-min-view-duration", cruiseCtlConfig.MinViewDuration.Load(), "the lower bound of authority for the controller, when active. This is the smallest amount of time a view is allowed to take.")
   155  		flags.DurationVar(&cruiseCtlMaxViewDurationFlag, "cruise-ctl-max-view-duration", cruiseCtlConfig.MaxViewDuration.Load(), "the upper bound of authority for the controller when active. This is the largest amount of time a view is allowed to take.")
   156  		flags.BoolVar(&cruiseCtlEnabledFlag, "cruise-ctl-enabled", cruiseCtlConfig.Enabled.Load(), "whether the block time controller is enabled; when disabled, the FallbackProposalDelay is used")
   157  		flags.UintVar(&chunkAlpha, "chunk-alpha", flow.DefaultChunkAssignmentAlpha, "number of verifiers that should be assigned to each chunk")
   158  		flags.UintVar(&requiredApprovalsForSealVerification, "required-verification-seal-approvals", flow.DefaultRequiredApprovalsForSealValidation, "minimum number of approvals that are required to verify a seal")
   159  		flags.UintVar(&requiredApprovalsForSealConstruction, "required-construction-seal-approvals", flow.DefaultRequiredApprovalsForSealConstruction, "minimum number of approvals that are required to construct a seal")
   160  		flags.BoolVar(&emergencySealing, "emergency-sealing-active", flow.DefaultEmergencySealingActive, "(de)activation of emergency sealing")
   161  		flags.BoolVar(&insecureAccessAPI, "insecure-access-api", false, "required if insecure GRPC connection should be used")
   162  		flags.StringSliceVar(&accessNodeIDS, "access-node-ids", []string{}, fmt.Sprintf("array of access node IDs sorted in priority order where the first ID in this array will get the first connection attempt and each subsequent ID after serves as a fallback. Minimum length %d. Use '*' for all IDs in protocol state.", common.DefaultAccessNodeIDSMinimum))
   163  		flags.DurationVar(&dkgMessagingEngineConfig.RetryBaseWait, "dkg-messaging-engine-retry-base-wait", dkgMessagingEngineConfig.RetryBaseWait, "the inter-attempt wait time for the first attempt (base of exponential retry)")
   164  		flags.Uint64Var(&dkgMessagingEngineConfig.RetryMax, "dkg-messaging-engine-retry-max", dkgMessagingEngineConfig.RetryMax, "the maximum number of retry attempts for an outbound DKG message")
   165  		flags.Uint64Var(&dkgMessagingEngineConfig.RetryJitterPercent, "dkg-messaging-engine-retry-jitter-percent", dkgMessagingEngineConfig.RetryJitterPercent, "the percentage of jitter to apply to each inter-attempt wait time")
   166  		flags.StringVar(&startupTimeString, "hotstuff-startup-time", cmd.NotSet, "specifies date and time (in ISO 8601 format) after which the consensus participant may enter the first view (e.g 1996-04-24T15:04:05-07:00)")
   167  		flags.DurationVar(&deprecatedFlagBlockRateDelay, "block-rate-delay", 0, "[deprecated in v0.30; Jun 2023] Use `cruise-ctl-*` flags instead, this flag has no effect and will eventually be removed")
   168  	}).ValidateFlags(func() error {
   169  		nodeBuilder.Logger.Info().Str("startup_time_str", startupTimeString).Msg("got startup_time_str")
   170  		if startupTimeString != cmd.NotSet {
   171  			t, err := time.Parse(time.RFC3339, startupTimeString)
   172  			if err != nil {
   173  				return fmt.Errorf("invalid start-time value: %w", err)
   174  			}
   175  			startupTime = t
   176  			nodeBuilder.Logger.Info().Time("startup_time", startupTime).Msg("got startup_time")
   177  		}
   178  		// parse target transition time string, if set
   179  		if cruiseCtlTargetTransitionTimeFlag != cruiseCtlConfig.TargetTransition.String() {
   180  			transitionTime, err := cruisectl.ParseTransition(cruiseCtlTargetTransitionTimeFlag)
   181  			if err != nil {
   182  				return fmt.Errorf("invalid epoch transition time string: %w", err)
   183  			}
   184  			cruiseCtlConfig.TargetTransition = *transitionTime
   185  		}
   186  		// convert local flag variables to atomic config variables, for dynamically updatable fields
   187  		if cruiseCtlEnabledFlag != cruiseCtlConfig.Enabled.Load() {
   188  			cruiseCtlConfig.Enabled.Store(cruiseCtlEnabledFlag)
   189  		}
   190  		if cruiseCtlFallbackProposalDurationFlag != cruiseCtlConfig.FallbackProposalDelay.Load() {
   191  			cruiseCtlConfig.FallbackProposalDelay.Store(cruiseCtlFallbackProposalDurationFlag)
   192  		}
   193  		if cruiseCtlMinViewDurationFlag != cruiseCtlConfig.MinViewDuration.Load() {
   194  			cruiseCtlConfig.MinViewDuration.Store(cruiseCtlMinViewDurationFlag)
   195  		}
   196  		if cruiseCtlMaxViewDurationFlag != cruiseCtlConfig.MaxViewDuration.Load() {
   197  			cruiseCtlConfig.MaxViewDuration.Store(cruiseCtlMaxViewDurationFlag)
   198  		}
   199  		// log a warning about deprecated flags
   200  		if deprecatedFlagBlockRateDelay > 0 {
   201  			nodeBuilder.Logger.Warn().Msg("A deprecated flag was specified (--block-rate-delay). This flag is deprecated as of v0.30 (Jun 2023), has no effect, and will eventually be removed.")
   202  		}
   203  		return nil
   204  	})
   205  
   206  	if err = nodeBuilder.Initialize(); err != nil {
   207  		nodeBuilder.Logger.Fatal().Err(err).Send()
   208  	}
   209  
   210  	nodeBuilder.
   211  		PreInit(cmd.DynamicStartPreInit).
   212  		ValidateRootSnapshot(badgerState.ValidRootSnapshotContainsEntityExpiryRange).
   213  		Module("consensus node metrics", func(node *cmd.NodeConfig) error {
   214  			conMetrics = metrics.NewConsensusCollector(node.Tracer, node.MetricsRegisterer)
   215  			return nil
   216  		}).
   217  		Module("dkg state", func(node *cmd.NodeConfig) error {
   218  			dkgState, err = bstorage.NewDKGState(node.Metrics.Cache, node.SecretsDB)
   219  			return err
   220  		}).
   221  		Module("beacon keys", func(node *cmd.NodeConfig) error {
   222  			safeBeaconKeys = bstorage.NewSafeBeaconPrivateKeys(dkgState)
   223  			return nil
   224  		}).
   225  		Module("updatable sealing config", func(node *cmd.NodeConfig) error {
   226  			setter, err := updatable_configs.NewSealingConfigs(
   227  				requiredApprovalsForSealConstruction,
   228  				requiredApprovalsForSealVerification,
   229  				chunkAlpha,
   230  				emergencySealing,
   231  			)
   232  			if err != nil {
   233  				return err
   234  			}
   235  
   236  			// update the getter with the setter, so other modules can only get, but not set
   237  			getSealingConfigs = setter
   238  
   239  			// admin tool is the only instance that have access to the setter interface, therefore, is
   240  			// the only module can change this config
   241  			err = node.ConfigManager.RegisterUintConfig("consensus-required-approvals-for-sealing",
   242  				setter.RequireApprovalsForSealConstructionDynamicValue,
   243  				setter.SetRequiredApprovalsForSealingConstruction)
   244  			return err
   245  		}).
   246  		Module("mutable follower state", func(node *cmd.NodeConfig) error {
   247  			// For now, we only support state implementations from package badger.
   248  			// If we ever support different implementations, the following can be replaced by a type-aware factory
   249  			state, ok := node.State.(*badgerState.State)
   250  			if !ok {
   251  				return fmt.Errorf("only implementations of type badger.State are currently supported but read-only state has type %T", node.State)
   252  			}
   253  
   254  			chunkAssigner, err = chmodule.NewChunkAssigner(chunkAlpha, node.State)
   255  			if err != nil {
   256  				return fmt.Errorf("could not instantiate assignment algorithm for chunk verification: %w", err)
   257  			}
   258  
   259  			receiptValidator = validation.NewReceiptValidator(
   260  				node.State,
   261  				node.Storage.Headers,
   262  				node.Storage.Index,
   263  				node.Storage.Results,
   264  				node.Storage.Seals)
   265  
   266  			sealValidator := validation.NewSealValidator(
   267  				node.State,
   268  				node.Storage.Headers,
   269  				node.Storage.Index,
   270  				node.Storage.Results,
   271  				node.Storage.Seals,
   272  				chunkAssigner,
   273  				getSealingConfigs,
   274  				conMetrics)
   275  
   276  			blockTimer, err = blocktimer.NewBlockTimer(minInterval, maxInterval)
   277  			if err != nil {
   278  				return err
   279  			}
   280  
   281  			mutableState, err = badgerState.NewFullConsensusState(
   282  				node.Logger,
   283  				node.Tracer,
   284  				node.ProtocolEvents,
   285  				state,
   286  				node.Storage.Index,
   287  				node.Storage.Payloads,
   288  				blockTimer,
   289  				receiptValidator,
   290  				sealValidator,
   291  			)
   292  			return err
   293  		}).
   294  		Module("random beacon key", func(node *cmd.NodeConfig) error {
   295  			// If this node was a participant in a spork, their beacon key for the
   296  			// first epoch was generated during the bootstrapping process and is
   297  			// specified in a private bootstrapping file. We load their key and
   298  			// store it in the db for the initial post-spork epoch for use going
   299  			// forward.
   300  			//
   301  			// If this node was not a participant in a spork, they joined at an
   302  			// epoch boundary, so they have no beacon key file (they will generate
   303  			// their first beacon private key through the DKG in the EpochSetup phase
   304  			// prior to their first epoch as network participant).
   305  
   306  			rootSnapshot := node.State.AtBlockID(node.FinalizedRootBlock.ID())
   307  			isSporkRoot, err := protocol.IsSporkRootSnapshot(rootSnapshot)
   308  			if err != nil {
   309  				return fmt.Errorf("could not check whether root snapshot is spork root: %w", err)
   310  			}
   311  			if !isSporkRoot {
   312  				node.Logger.Info().Msg("node starting from mid-spork snapshot, will not read spork random beacon key file")
   313  				return nil
   314  			}
   315  
   316  			// If the node has a beacon key file, then save it to the secrets database
   317  			// as the beacon key for the epoch of the root snapshot.
   318  			beaconPrivateKey, err = loadBeaconPrivateKey(node.BaseConfig.BootstrapDir, node.NodeID)
   319  			if errors.Is(err, os.ErrNotExist) {
   320  				return fmt.Errorf("node is starting from spork root snapshot, but does not have spork random beacon key file: %w", err)
   321  			}
   322  			if err != nil {
   323  				return fmt.Errorf("could not load beacon key file: %w", err)
   324  			}
   325  
   326  			rootEpoch := node.State.AtBlockID(node.FinalizedRootBlock.ID()).Epochs().Current()
   327  			epochCounter, err := rootEpoch.Counter()
   328  			if err != nil {
   329  				return fmt.Errorf("could not get root epoch counter: %w", err)
   330  			}
   331  
   332  			// confirm the beacon key file matches the canonical public keys
   333  			rootDKG, err := rootEpoch.DKG()
   334  			if err != nil {
   335  				return fmt.Errorf("could not get dkg for root epoch: %w", err)
   336  			}
   337  			myBeaconPublicKeyShare, err := rootDKG.KeyShare(node.NodeID)
   338  			if err != nil {
   339  				return fmt.Errorf("could not get my beacon public key share for root epoch: %w", err)
   340  			}
   341  
   342  			if !myBeaconPublicKeyShare.Equals(beaconPrivateKey.PrivateKey.PublicKey()) {
   343  				return fmt.Errorf("configured beacon key is inconsistent with this node's canonical public beacon key (%s!=%s)",
   344  					beaconPrivateKey.PrivateKey.PublicKey(),
   345  					myBeaconPublicKeyShare)
   346  			}
   347  
   348  			// store my beacon key for the first epoch post-spork
   349  			err = dkgState.InsertMyBeaconPrivateKey(epochCounter, beaconPrivateKey.PrivateKey)
   350  			if err != nil && !errors.Is(err, storage.ErrAlreadyExists) {
   351  				return err
   352  			}
   353  			// mark the root DKG as successful, so it is considered safe to use the key
   354  			err = dkgState.SetDKGEndState(epochCounter, flow.DKGEndStateSuccess)
   355  			if err != nil && !errors.Is(err, storage.ErrAlreadyExists) {
   356  				return err
   357  			}
   358  
   359  			return nil
   360  		}).
   361  		Module("collection guarantees mempool", func(node *cmd.NodeConfig) error {
   362  			guarantees, err = stdmap.NewGuarantees(guaranteeLimit)
   363  			return err
   364  		}).
   365  		Module("execution receipts mempool", func(node *cmd.NodeConfig) error {
   366  			receipts = consensusMempools.NewExecutionTree()
   367  			// registers size method of backend for metrics
   368  			err = node.Metrics.Mempool.Register(metrics.ResourceReceipt, receipts.Size)
   369  			if err != nil {
   370  				return fmt.Errorf("could not register backend metric: %w", err)
   371  			}
   372  			return nil
   373  		}).
   374  		Module("block seals mempool", func(node *cmd.NodeConfig) error {
   375  			// use a custom ejector, so we don't eject seals that would break
   376  			// the chain of seals
   377  			rawMempool := stdmap.NewIncorporatedResultSeals(sealLimit)
   378  			multipleReceiptsFilterMempool := consensusMempools.NewIncorporatedResultSeals(rawMempool, node.Storage.Receipts)
   379  			seals, err = consensusMempools.NewExecStateForkSuppressor(
   380  				multipleReceiptsFilterMempool,
   381  				consensusMempools.LogForkAndCrash(node.Logger),
   382  				node.DB,
   383  				node.Logger,
   384  			)
   385  			if err != nil {
   386  				return fmt.Errorf("failed to wrap seals mempool into ExecStateForkSuppressor: %w", err)
   387  			}
   388  			err = node.Metrics.Mempool.Register(metrics.ResourcePendingIncorporatedSeal, seals.Size)
   389  			return nil
   390  		}).
   391  		Module("pending receipts mempool", func(node *cmd.NodeConfig) error {
   392  			pendingReceipts = stdmap.NewPendingReceipts(node.Storage.Headers, pendingReceiptsLimit)
   393  			return nil
   394  		}).
   395  		Module("hotstuff main metrics", func(node *cmd.NodeConfig) error {
   396  			mainMetrics = metrics.NewHotstuffCollector(node.RootChainID)
   397  			return nil
   398  		}).
   399  		Module("sync core", func(node *cmd.NodeConfig) error {
   400  			syncCore, err = chainsync.New(node.Logger, node.SyncCoreConfig, metrics.NewChainSyncCollector(node.RootChainID), node.RootChainID)
   401  			return err
   402  		}).
   403  		Module("follower distributor", func(node *cmd.NodeConfig) error {
   404  			followerDistributor = pubsub.NewFollowerDistributor()
   405  			return nil
   406  		}).
   407  		Module("machine account config", func(node *cmd.NodeConfig) error {
   408  			machineAccountInfo, err = cmd.LoadNodeMachineAccountInfoFile(node.BootstrapDir, node.NodeID)
   409  			return err
   410  		}).
   411  		Module("sdk client connection options", func(node *cmd.NodeConfig) error {
   412  			anIDS, err := common.ValidateAccessNodeIDSFlag(accessNodeIDS, node.RootChainID, node.State.Sealed())
   413  			if err != nil {
   414  				return fmt.Errorf("failed to validate flag --access-node-ids %w", err)
   415  			}
   416  
   417  			flowClientConfigs, err = common.FlowClientConfigs(anIDS, insecureAccessAPI, node.State.Sealed())
   418  			if err != nil {
   419  				return fmt.Errorf("failed to prepare flow client connection configs for each access node id %w", err)
   420  			}
   421  
   422  			return nil
   423  		}).
   424  		Component("machine account config validator", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
   425  			// @TODO use fallback logic for flowClient similar to DKG/QC contract clients
   426  			flowClient, err := common.FlowClient(flowClientConfigs[0])
   427  			if err != nil {
   428  				return nil, fmt.Errorf("failed to get flow client connection option for access node (0): %s %w", flowClientConfigs[0].AccessAddress, err)
   429  			}
   430  
   431  			// disable balance checks for transient networks, which do not have transaction fees
   432  			var opts []epochs.MachineAccountValidatorConfigOption
   433  			if node.RootChainID.Transient() {
   434  				opts = append(opts, epochs.WithoutBalanceChecks)
   435  			}
   436  			validator, err := epochs.NewMachineAccountConfigValidator(
   437  				node.Logger,
   438  				flowClient,
   439  				flow.RoleCollection,
   440  				*machineAccountInfo,
   441  				opts...,
   442  			)
   443  			return validator, err
   444  		}).
   445  		Component("sealing engine", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
   446  
   447  			sealingTracker := tracker.NewSealingTracker(node.Logger, node.Storage.Headers, node.Storage.Receipts, seals)
   448  
   449  			e, err := sealing.NewEngine(
   450  				node.Logger,
   451  				node.Tracer,
   452  				conMetrics,
   453  				node.Metrics.Engine,
   454  				node.Metrics.Mempool,
   455  				sealingTracker,
   456  				node.EngineRegistry,
   457  				node.Me,
   458  				node.Storage.Headers,
   459  				node.Storage.Payloads,
   460  				node.Storage.Results,
   461  				node.Storage.Index,
   462  				node.State,
   463  				node.Storage.Seals,
   464  				chunkAssigner,
   465  				seals,
   466  				getSealingConfigs,
   467  			)
   468  
   469  			// subscribe for finalization events from hotstuff
   470  			followerDistributor.AddOnBlockFinalizedConsumer(e.OnFinalizedBlock)
   471  			followerDistributor.AddOnBlockIncorporatedConsumer(e.OnBlockIncorporated)
   472  
   473  			return e, err
   474  		}).
   475  		Component("matching engine", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
   476  			receiptRequester, err = requester.New(
   477  				node.Logger,
   478  				node.Metrics.Engine,
   479  				node.EngineRegistry,
   480  				node.Me,
   481  				node.State,
   482  				channels.RequestReceiptsByBlockID,
   483  				filter.HasRole(flow.RoleExecution),
   484  				func() flow.Entity { return &flow.ExecutionReceipt{} },
   485  				requester.WithRetryInitial(2*time.Second),
   486  				requester.WithRetryMaximum(30*time.Second),
   487  			)
   488  			if err != nil {
   489  				return nil, err
   490  			}
   491  
   492  			core := matching.NewCore(
   493  				node.Logger,
   494  				node.Tracer,
   495  				conMetrics,
   496  				node.Metrics.Mempool,
   497  				node.State,
   498  				node.Storage.Headers,
   499  				node.Storage.Receipts,
   500  				receipts,
   501  				pendingReceipts,
   502  				seals,
   503  				receiptValidator,
   504  				receiptRequester,
   505  				matching.DefaultConfig(),
   506  			)
   507  
   508  			e, err := matching.NewEngine(
   509  				node.Logger,
   510  				node.EngineRegistry,
   511  				node.Me,
   512  				node.Metrics.Engine,
   513  				node.Metrics.Mempool,
   514  				node.State,
   515  				node.Storage.Receipts,
   516  				node.Storage.Index,
   517  				core,
   518  			)
   519  			if err != nil {
   520  				return nil, err
   521  			}
   522  
   523  			// subscribe engine to inputs from other node-internal components
   524  			receiptRequester.WithHandle(e.HandleReceipt)
   525  			followerDistributor.AddOnBlockFinalizedConsumer(e.OnFinalizedBlock)
   526  			followerDistributor.AddOnBlockIncorporatedConsumer(e.OnBlockIncorporated)
   527  
   528  			return e, err
   529  		}).
   530  		Component("ingestion engine", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
   531  			core := ingestion.NewCore(
   532  				node.Logger,
   533  				node.Tracer,
   534  				node.Metrics.Mempool,
   535  				node.State,
   536  				node.Storage.Headers,
   537  				guarantees,
   538  			)
   539  
   540  			ing, err := ingestion.New(
   541  				node.Logger,
   542  				node.Metrics.Engine,
   543  				node.EngineRegistry,
   544  				node.Me,
   545  				core,
   546  			)
   547  
   548  			return ing, err
   549  		}).
   550  		Component("hotstuff committee", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
   551  			committee, err = committees.NewConsensusCommittee(node.State, node.Me.NodeID())
   552  			node.ProtocolEvents.AddConsumer(committee)
   553  			return committee, err
   554  		}).
   555  		Component("epoch lookup", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
   556  			epochLookup, err = epochs.NewEpochLookup(node.State)
   557  			node.ProtocolEvents.AddConsumer(epochLookup)
   558  			return epochLookup, err
   559  		}).
   560  		Component("hotstuff modules", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
   561  			// initialize the block finalizer
   562  			finalize := finalizer.NewFinalizer(
   563  				node.DB,
   564  				node.Storage.Headers,
   565  				mutableState,
   566  				node.Tracer,
   567  				finalizer.WithCleanup(finalizer.CleanupMempools(
   568  					node.Metrics.Mempool,
   569  					conMetrics,
   570  					node.Storage.Payloads,
   571  					guarantees,
   572  					seals,
   573  				)),
   574  			)
   575  
   576  			// wrap Main consensus committee with metrics
   577  			wrappedCommittee := committees.NewMetricsWrapper(committee, mainMetrics) // wrapper for measuring time spent determining consensus committee relations
   578  
   579  			beaconKeyStore := hotsignature.NewEpochAwareRandomBeaconKeyStore(epochLookup, safeBeaconKeys)
   580  
   581  			// initialize the combined signer for hotstuff
   582  			var signer hotstuff.Signer
   583  			signer = verification.NewCombinedSigner(
   584  				node.Me,
   585  				beaconKeyStore,
   586  			)
   587  			signer = verification.NewMetricsWrapper(signer, mainMetrics) // wrapper for measuring time spent with crypto-related operations
   588  
   589  			// create consensus logger
   590  			logger := createLogger(node.Logger, node.RootChainID)
   591  
   592  			telemetryConsumer := notifications.NewTelemetryConsumer(logger)
   593  			slashingViolationConsumer := notifications.NewSlashingViolationsConsumer(nodeBuilder.Logger)
   594  			followerDistributor.AddProposalViolationConsumer(slashingViolationConsumer)
   595  
   596  			// initialize a logging notifier for hotstuff
   597  			notifier := createNotifier(
   598  				logger,
   599  				mainMetrics,
   600  			)
   601  
   602  			notifier.AddParticipantConsumer(telemetryConsumer)
   603  			notifier.AddCommunicatorConsumer(telemetryConsumer)
   604  			notifier.AddFinalizationConsumer(telemetryConsumer)
   605  			notifier.AddFollowerConsumer(followerDistributor)
   606  
   607  			// initialize the persister
   608  			persist := persister.New(node.DB, node.RootChainID)
   609  
   610  			finalizedBlock, err := node.State.Final().Head()
   611  			if err != nil {
   612  				return nil, err
   613  			}
   614  
   615  			forks, err := consensus.NewForks(
   616  				finalizedBlock,
   617  				node.Storage.Headers,
   618  				finalize,
   619  				notifier,
   620  				node.FinalizedRootBlock.Header,
   621  				node.RootQC,
   622  			)
   623  			if err != nil {
   624  				return nil, err
   625  			}
   626  
   627  			// create producer and connect it to consumers
   628  			voteAggregationDistributor := pubsub.NewVoteAggregationDistributor()
   629  			voteAggregationDistributor.AddVoteCollectorConsumer(telemetryConsumer)
   630  			voteAggregationDistributor.AddVoteAggregationViolationConsumer(slashingViolationConsumer)
   631  
   632  			validator := consensus.NewValidator(mainMetrics, wrappedCommittee)
   633  			voteProcessorFactory := votecollector.NewCombinedVoteProcessorFactory(wrappedCommittee, voteAggregationDistributor.OnQcConstructedFromVotes)
   634  			lowestViewForVoteProcessing := finalizedBlock.View + 1
   635  			voteAggregator, err := consensus.NewVoteAggregator(
   636  				logger,
   637  				mainMetrics,
   638  				node.Metrics.Engine,
   639  				node.Metrics.Mempool,
   640  				lowestViewForVoteProcessing,
   641  				voteAggregationDistributor,
   642  				voteProcessorFactory,
   643  				followerDistributor)
   644  			if err != nil {
   645  				return nil, fmt.Errorf("could not initialize vote aggregator: %w", err)
   646  			}
   647  
   648  			// create producer and connect it to consumers
   649  			timeoutAggregationDistributor := pubsub.NewTimeoutAggregationDistributor()
   650  			timeoutAggregationDistributor.AddTimeoutCollectorConsumer(telemetryConsumer)
   651  			timeoutAggregationDistributor.AddTimeoutAggregationViolationConsumer(slashingViolationConsumer)
   652  
   653  			timeoutProcessorFactory := timeoutcollector.NewTimeoutProcessorFactory(
   654  				logger,
   655  				timeoutAggregationDistributor,
   656  				committee,
   657  				validator,
   658  				msig.ConsensusTimeoutTag,
   659  			)
   660  			timeoutAggregator, err := consensus.NewTimeoutAggregator(
   661  				logger,
   662  				mainMetrics,
   663  				node.Metrics.Engine,
   664  				node.Metrics.Mempool,
   665  				notifier,
   666  				timeoutProcessorFactory,
   667  				timeoutAggregationDistributor,
   668  				lowestViewForVoteProcessing,
   669  			)
   670  			if err != nil {
   671  				return nil, fmt.Errorf("could not initialize timeout aggregator: %w", err)
   672  			}
   673  
   674  			hotstuffModules = &consensus.HotstuffModules{
   675  				Notifier:                    notifier,
   676  				Committee:                   wrappedCommittee,
   677  				Signer:                      signer,
   678  				Persist:                     persist,
   679  				VoteCollectorDistributor:    voteAggregationDistributor.VoteCollectorDistributor,
   680  				TimeoutCollectorDistributor: timeoutAggregationDistributor.TimeoutCollectorDistributor,
   681  				Forks:                       forks,
   682  				Validator:                   validator,
   683  				VoteAggregator:              voteAggregator,
   684  				TimeoutAggregator:           timeoutAggregator,
   685  			}
   686  
   687  			return util.MergeReadyDone(voteAggregator, timeoutAggregator), nil
   688  		}).
   689  		Component("block rate cruise control", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
   690  			livenessData, err := hotstuffModules.Persist.GetLivenessData()
   691  			if err != nil {
   692  				return nil, err
   693  			}
   694  			ctl, err := cruisectl.NewBlockTimeController(node.Logger, metrics.NewCruiseCtlMetrics(), cruiseCtlConfig, node.State, livenessData.CurrentView)
   695  			if err != nil {
   696  				return nil, err
   697  			}
   698  			proposalDurProvider = ctl
   699  			hotstuffModules.Notifier.AddOnBlockIncorporatedConsumer(ctl.OnBlockIncorporated)
   700  			node.ProtocolEvents.AddConsumer(ctl)
   701  
   702  			// set up admin commands for dynamically updating configs
   703  			err = node.ConfigManager.RegisterBoolConfig("cruise-ctl-enabled", cruiseCtlConfig.GetEnabled, cruiseCtlConfig.SetEnabled)
   704  			if err != nil {
   705  				return nil, err
   706  			}
   707  			err = node.ConfigManager.RegisterDurationConfig("cruise-ctl-fallback-proposal-duration", cruiseCtlConfig.GetFallbackProposalDuration, cruiseCtlConfig.SetFallbackProposalDuration)
   708  			if err != nil {
   709  				return nil, err
   710  			}
   711  			err = node.ConfigManager.RegisterDurationConfig("cruise-ctl-min-view-duration", cruiseCtlConfig.GetMinViewDuration, cruiseCtlConfig.SetMinViewDuration)
   712  			if err != nil {
   713  				return nil, err
   714  			}
   715  			err = node.ConfigManager.RegisterDurationConfig("cruise-ctl-max-view-duration", cruiseCtlConfig.GetMaxViewDuration, cruiseCtlConfig.SetMaxViewDuration)
   716  			if err != nil {
   717  				return nil, err
   718  			}
   719  
   720  			return ctl, nil
   721  		}).
   722  		Component("consensus participant", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
   723  			// initialize the block builder
   724  			var build module.Builder
   725  			build, err = builder.NewBuilder(
   726  				node.Metrics.Mempool,
   727  				node.DB,
   728  				mutableState,
   729  				node.Storage.Headers,
   730  				node.Storage.Seals,
   731  				node.Storage.Index,
   732  				node.Storage.Blocks,
   733  				node.Storage.Results,
   734  				node.Storage.Receipts,
   735  				guarantees,
   736  				seals,
   737  				receipts,
   738  				node.Tracer,
   739  				builder.WithBlockTimer(blockTimer),
   740  				builder.WithMaxSealCount(maxSealPerBlock),
   741  				builder.WithMaxGuaranteeCount(maxGuaranteePerBlock),
   742  			)
   743  			if err != nil {
   744  				return nil, fmt.Errorf("could not initialized block builder: %w", err)
   745  			}
   746  			build = blockproducer.NewMetricsWrapper(build, mainMetrics) // wrapper for measuring time spent building block payload component
   747  
   748  			opts := []consensus.Option{
   749  				consensus.WithMinTimeout(hotstuffMinTimeout),
   750  				consensus.WithTimeoutAdjustmentFactor(hotstuffTimeoutAdjustmentFactor),
   751  				consensus.WithHappyPathMaxRoundFailures(hotstuffHappyPathMaxRoundFailures),
   752  				consensus.WithProposalDurationProvider(proposalDurProvider),
   753  			}
   754  
   755  			if !startupTime.IsZero() {
   756  				opts = append(opts, consensus.WithStartupTime(startupTime))
   757  			}
   758  			finalizedBlock, pending, err := recovery.FindLatest(node.State, node.Storage.Headers)
   759  			if err != nil {
   760  				return nil, err
   761  			}
   762  
   763  			// initialize hotstuff consensus algorithm
   764  			hot, err = consensus.NewParticipant(
   765  				createLogger(node.Logger, node.RootChainID),
   766  				mainMetrics,
   767  				node.Metrics.Mempool,
   768  				build,
   769  				finalizedBlock,
   770  				pending,
   771  				hotstuffModules,
   772  				opts...,
   773  			)
   774  			if err != nil {
   775  				return nil, fmt.Errorf("could not initialize hotstuff engine: %w", err)
   776  			}
   777  			return hot, nil
   778  		}).
   779  		Component("consensus compliance engine", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
   780  			// initialize the pending blocks cache
   781  			proposals := buffer.NewPendingBlocks()
   782  
   783  			logger := createLogger(node.Logger, node.RootChainID)
   784  			complianceCore, err := compliance.NewCore(
   785  				logger,
   786  				node.Metrics.Engine,
   787  				node.Metrics.Mempool,
   788  				mainMetrics,
   789  				node.Metrics.Compliance,
   790  				followerDistributor,
   791  				node.Tracer,
   792  				node.Storage.Headers,
   793  				node.Storage.Payloads,
   794  				mutableState,
   795  				proposals,
   796  				syncCore,
   797  				hotstuffModules.Validator,
   798  				hot,
   799  				hotstuffModules.VoteAggregator,
   800  				hotstuffModules.TimeoutAggregator,
   801  				node.ComplianceConfig,
   802  			)
   803  			if err != nil {
   804  				return nil, fmt.Errorf("could not initialize compliance core: %w", err)
   805  			}
   806  
   807  			// initialize the compliance engine
   808  			comp, err = compliance.NewEngine(
   809  				logger,
   810  				node.Me,
   811  				complianceCore,
   812  			)
   813  			if err != nil {
   814  				return nil, fmt.Errorf("could not initialize compliance engine: %w", err)
   815  			}
   816  			followerDistributor.AddOnBlockFinalizedConsumer(comp.OnFinalizedBlock)
   817  
   818  			return comp, nil
   819  		}).
   820  		Component("consensus message hub", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
   821  			messageHub, err := message_hub.NewMessageHub(
   822  				createLogger(node.Logger, node.RootChainID),
   823  				node.Metrics.Engine,
   824  				node.EngineRegistry,
   825  				node.Me,
   826  				comp,
   827  				hot,
   828  				hotstuffModules.VoteAggregator,
   829  				hotstuffModules.TimeoutAggregator,
   830  				node.State,
   831  				node.Storage.Payloads,
   832  			)
   833  			if err != nil {
   834  				return nil, fmt.Errorf("could not create consensus message hub: %w", err)
   835  			}
   836  			hotstuffModules.Notifier.AddConsumer(messageHub)
   837  			return messageHub, nil
   838  		}).
   839  		Component("sync engine", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
   840  			spamConfig, err := synceng.NewSpamDetectionConfig()
   841  			if err != nil {
   842  				return nil, fmt.Errorf("could not initialize spam detection config: %w", err)
   843  			}
   844  
   845  			sync, err := synceng.New(
   846  				node.Logger,
   847  				node.Metrics.Engine,
   848  				node.EngineRegistry,
   849  				node.Me,
   850  				node.State,
   851  				node.Storage.Blocks,
   852  				comp,
   853  				syncCore,
   854  				node.SyncEngineIdentifierProvider,
   855  				spamConfig,
   856  			)
   857  			if err != nil {
   858  				return nil, fmt.Errorf("could not initialize synchronization engine: %w", err)
   859  			}
   860  			followerDistributor.AddFinalizationConsumer(sync)
   861  
   862  			return sync, nil
   863  		}).
   864  		Component("receipt requester engine", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
   865  			// created with sealing engine
   866  			return receiptRequester, nil
   867  		}).
   868  		Component("DKG messaging engine", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
   869  
   870  			// brokerTunnel is used to forward messages between the DKG
   871  			// messaging engine and the DKG broker/controller
   872  			dkgBrokerTunnel = dkgmodule.NewBrokerTunnel()
   873  
   874  			// messagingEngine is a network engine that is used by nodes to
   875  			// exchange private DKG messages
   876  			messagingEngine, err := dkgeng.NewMessagingEngine(
   877  				node.Logger,
   878  				node.EngineRegistry,
   879  				node.Me,
   880  				dkgBrokerTunnel,
   881  				node.Metrics.Mempool,
   882  				dkgMessagingEngineConfig,
   883  			)
   884  			if err != nil {
   885  				return nil, fmt.Errorf("could not initialize DKG messaging engine: %w", err)
   886  			}
   887  
   888  			return messagingEngine, nil
   889  		}).
   890  		Component("DKG reactor engine", func(node *cmd.NodeConfig) (module.ReadyDoneAware, error) {
   891  			// the viewsObserver is used by the reactor engine to subscribe to
   892  			// new views being finalized
   893  			viewsObserver := gadgets.NewViews()
   894  			node.ProtocolEvents.AddConsumer(viewsObserver)
   895  
   896  			// construct DKG contract client
   897  			dkgContractClients, err := createDKGContractClients(node, machineAccountInfo, flowClientConfigs)
   898  			if err != nil {
   899  				return nil, fmt.Errorf("could not create dkg contract client %w", err)
   900  			}
   901  
   902  			// the reactor engine reacts to new views being finalized and drives the
   903  			// DKG protocol
   904  			reactorEngine := dkgeng.NewReactorEngine(
   905  				node.Logger,
   906  				node.Me,
   907  				node.State,
   908  				dkgState,
   909  				dkgmodule.NewControllerFactory(
   910  					node.Logger,
   911  					node.Me,
   912  					dkgContractClients,
   913  					dkgBrokerTunnel,
   914  				),
   915  				viewsObserver,
   916  			)
   917  
   918  			// reactorEngine consumes the EpochSetupPhaseStarted event
   919  			node.ProtocolEvents.AddConsumer(reactorEngine)
   920  
   921  			return reactorEngine, nil
   922  		})
   923  
   924  	node, err := nodeBuilder.Build()
   925  	if err != nil {
   926  		nodeBuilder.Logger.Fatal().Err(err).Send()
   927  	}
   928  	node.Run()
   929  }
   930  
   931  func loadBeaconPrivateKey(dir string, myID flow.Identifier) (*encodable.RandomBeaconPrivKey, error) {
   932  	path := fmt.Sprintf(bootstrap.PathRandomBeaconPriv, myID)
   933  	data, err := io.ReadFile(filepath.Join(dir, path))
   934  	if err != nil {
   935  		return nil, err
   936  	}
   937  
   938  	var priv encodable.RandomBeaconPrivKey
   939  	err = json.Unmarshal(data, &priv)
   940  	if err != nil {
   941  		return nil, err
   942  	}
   943  	return &priv, nil
   944  }
   945  
   946  // createDKGContractClient creates an dkgContractClient
   947  func createDKGContractClient(node *cmd.NodeConfig, machineAccountInfo *bootstrap.NodeMachineAccountInfo, flowClient *client.Client, anID flow.Identifier) (module.DKGContractClient, error) {
   948  	var dkgClient module.DKGContractClient
   949  
   950  	contracts := systemcontracts.SystemContractsForChain(node.RootChainID)
   951  	dkgContractAddress := contracts.DKG.Address.Hex()
   952  
   953  	// construct signer from private key
   954  	sk, err := crypto.DecodePrivateKey(machineAccountInfo.SigningAlgorithm, machineAccountInfo.EncodedPrivateKey)
   955  	if err != nil {
   956  		return nil, fmt.Errorf("could not decode private key from hex: %w", err)
   957  	}
   958  
   959  	txSigner, err := crypto.NewInMemorySigner(sk, machineAccountInfo.HashAlgorithm)
   960  	if err != nil {
   961  		return nil, fmt.Errorf("could not create in-memory signer: %w", err)
   962  	}
   963  
   964  	// create actual dkg contract client, all flags and machine account info file found
   965  	dkgClient = dkgmodule.NewClient(
   966  		node.Logger,
   967  		flowClient,
   968  		anID,
   969  		txSigner,
   970  		dkgContractAddress,
   971  		machineAccountInfo.Address,
   972  		machineAccountInfo.KeyIndex,
   973  	)
   974  
   975  	return dkgClient, nil
   976  }
   977  
   978  // createDKGContractClients creates an array dkgContractClient that is sorted by retry fallback priority
   979  func createDKGContractClients(node *cmd.NodeConfig, machineAccountInfo *bootstrap.NodeMachineAccountInfo, flowClientOpts []*common.FlowClientConfig) ([]module.DKGContractClient, error) {
   980  	dkgClients := make([]module.DKGContractClient, 0)
   981  
   982  	for _, opt := range flowClientOpts {
   983  		flowClient, err := common.FlowClient(opt)
   984  		if err != nil {
   985  			return nil, fmt.Errorf("failed to create flow client for dkg contract client with options: %s %w", flowClientOpts, err)
   986  		}
   987  
   988  		node.Logger.Info().Msgf("created dkg contract client with opts: %s", opt.String())
   989  		dkgClient, err := createDKGContractClient(node, machineAccountInfo, flowClient, opt.AccessNodeID)
   990  		if err != nil {
   991  			return nil, fmt.Errorf("failed to create dkg contract client with flow client options: %s %w", flowClientOpts, err)
   992  		}
   993  
   994  		dkgClients = append(dkgClients, dkgClient)
   995  	}
   996  
   997  	return dkgClients, nil
   998  }