github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/epochs/machine_account.go (about)

     1  package epochs
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"strconv"
     8  	"time"
     9  
    10  	"github.com/onflow/cadence"
    11  	"github.com/rs/zerolog"
    12  	"github.com/sethvargo/go-retry"
    13  
    14  	sdk "github.com/onflow/flow-go-sdk"
    15  	client "github.com/onflow/flow-go-sdk/access/grpc"
    16  	sdkcrypto "github.com/onflow/flow-go-sdk/crypto"
    17  
    18  	"github.com/onflow/flow-go/model/bootstrap"
    19  	"github.com/onflow/flow-go/model/flow"
    20  	"github.com/onflow/flow-go/module"
    21  	"github.com/onflow/flow-go/module/component"
    22  	"github.com/onflow/flow-go/module/irrecoverable"
    23  )
    24  
    25  var (
    26  	// Hard and soft balance limits for collection and consensus nodes.
    27  	// We will log a warning once for a soft limit, and will log an error
    28  	// in perpetuity for a hard limit.
    29  	// Taken from https://www.notion.so/dapperlabs/Machine-Account-f3c293593ea442a39614fcebf705a132
    30  	// TODO update these for FLIP74
    31  
    32  	defaultSoftMinBalanceLN cadence.UFix64
    33  	defaultHardMinBalanceLN cadence.UFix64
    34  	defaultSoftMinBalanceSN cadence.UFix64
    35  	defaultHardMinBalanceSN cadence.UFix64
    36  )
    37  
    38  const (
    39  	recommendedMinBalanceLN = 0.002
    40  	recommendedMinBalanceSN = 0.05
    41  )
    42  
    43  func init() {
    44  	var err error
    45  	defaultSoftMinBalanceLN, err = cadence.NewUFix64("0.0025")
    46  	if err != nil {
    47  		panic(fmt.Errorf("could not convert soft min balance for LN: %w", err))
    48  	}
    49  	defaultHardMinBalanceLN, err = cadence.NewUFix64("0.002")
    50  	if err != nil {
    51  		panic(fmt.Errorf("could not convert hard min balance for LN: %w", err))
    52  	}
    53  	defaultSoftMinBalanceSN, err = cadence.NewUFix64("0.125")
    54  	if err != nil {
    55  		panic(fmt.Errorf("could not convert soft min balance for SN: %w", err))
    56  	}
    57  	defaultHardMinBalanceSN, err = cadence.NewUFix64("0.05")
    58  	if err != nil {
    59  		panic(fmt.Errorf("could not convert hard min balance for SN: %w", err))
    60  	}
    61  
    62  	// sanity checks
    63  	if asFloat, err := ufix64Tofloat64(defaultHardMinBalanceLN); err != nil {
    64  		panic(err)
    65  	} else if asFloat != recommendedMinBalanceLN {
    66  		panic(fmt.Errorf("failed sanity check: %f!=%f", asFloat, recommendedMinBalanceLN))
    67  	}
    68  	if asFloat, err := ufix64Tofloat64(defaultHardMinBalanceSN); err != nil {
    69  		panic(err)
    70  	} else if asFloat != recommendedMinBalanceSN {
    71  		panic(fmt.Errorf("failed sanity check: %f!=%f", asFloat, recommendedMinBalanceSN))
    72  	}
    73  }
    74  
    75  const (
    76  	checkMachineAccountRetryBase      = time.Second * 30
    77  	checkMachineAccountRetryMax       = time.Minute * 30
    78  	checkMachineAccountRetryJitterPct = 10
    79  )
    80  
    81  // checkMachineAccountRetryBackoff returns the default backoff for checking machine account configs.
    82  //   - exponential backoff with base of 30s
    83  //   - maximum inter-check wait of 30
    84  //   - 10% jitter
    85  func checkMachineAccountRetryBackoff() retry.Backoff {
    86  	backoff := retry.NewExponential(checkMachineAccountRetryBase)
    87  	backoff = retry.WithCappedDuration(checkMachineAccountRetryMax, backoff)
    88  	backoff = retry.WithJitterPercent(checkMachineAccountRetryJitterPct, backoff)
    89  	return backoff
    90  }
    91  
    92  // MachineAccountValidatorConfig defines configuration options for MachineAccountConfigValidator.
    93  type MachineAccountValidatorConfig struct {
    94  	SoftMinBalanceLN cadence.UFix64
    95  	HardMinBalanceLN cadence.UFix64
    96  	SoftMinBalanceSN cadence.UFix64
    97  	HardMinBalanceSN cadence.UFix64
    98  }
    99  
   100  func DefaultMachineAccountValidatorConfig() MachineAccountValidatorConfig {
   101  	return MachineAccountValidatorConfig{
   102  		SoftMinBalanceLN: defaultSoftMinBalanceLN,
   103  		HardMinBalanceLN: defaultHardMinBalanceLN,
   104  		SoftMinBalanceSN: defaultSoftMinBalanceSN,
   105  		HardMinBalanceSN: defaultHardMinBalanceSN,
   106  	}
   107  }
   108  
   109  // WithoutBalanceChecks sets minimum balances to 0 to effectively disable minimum
   110  // balance checks. This is useful for test networks where transaction fees are
   111  // disabled.
   112  func WithoutBalanceChecks(conf *MachineAccountValidatorConfig) {
   113  	conf.SoftMinBalanceLN = 0
   114  	conf.HardMinBalanceLN = 0
   115  	conf.SoftMinBalanceSN = 0
   116  	conf.HardMinBalanceSN = 0
   117  }
   118  
   119  type MachineAccountValidatorConfigOption func(*MachineAccountValidatorConfig)
   120  
   121  // MachineAccountConfigValidator is used to validate that a machine account is
   122  // configured correctly.
   123  type MachineAccountConfigValidator struct {
   124  	config  MachineAccountValidatorConfig
   125  	metrics module.MachineAccountMetrics
   126  	log     zerolog.Logger
   127  	client  *client.Client
   128  	role    flow.Role
   129  	info    bootstrap.NodeMachineAccountInfo
   130  
   131  	component.Component
   132  }
   133  
   134  func NewMachineAccountConfigValidator(
   135  	log zerolog.Logger,
   136  	flowClient *client.Client,
   137  	role flow.Role,
   138  	info bootstrap.NodeMachineAccountInfo,
   139  	metrics module.MachineAccountMetrics,
   140  	opts ...MachineAccountValidatorConfigOption,
   141  ) (*MachineAccountConfigValidator, error) {
   142  
   143  	conf := DefaultMachineAccountValidatorConfig()
   144  	for _, apply := range opts {
   145  		apply(&conf)
   146  	}
   147  
   148  	validator := &MachineAccountConfigValidator{
   149  		config:  conf,
   150  		log:     log.With().Str("component", "machine_account_config_validator").Logger(),
   151  		client:  flowClient,
   152  		role:    role,
   153  		info:    info,
   154  		metrics: metrics,
   155  	}
   156  
   157  	// report recommended min balance once at construction
   158  	switch role {
   159  	case flow.RoleCollection:
   160  		validator.metrics.RecommendedMinBalance(recommendedMinBalanceLN)
   161  	case flow.RoleConsensus:
   162  		validator.metrics.RecommendedMinBalance(recommendedMinBalanceSN)
   163  	default:
   164  		return nil, fmt.Errorf("invalid role: %s", role)
   165  	}
   166  
   167  	validator.Component = component.NewComponentManagerBuilder().
   168  		AddWorker(validator.reportMachineAccountConfigWorker).
   169  		Build()
   170  
   171  	return validator, nil
   172  }
   173  
   174  // reportMachineAccountConfigWorker is a worker function that periodically checks
   175  // and reports on the health of the node's configured machine account.
   176  // When a misconfiguration or insufficient account balance is detected, the worker
   177  // will report metrics and log specific information about what is wrong.
   178  //
   179  // This worker runs perpetually in the background, executing once per 30 minutes
   180  // in the steady state. It will execute more frequently right after startup.
   181  func (validator *MachineAccountConfigValidator) reportMachineAccountConfigWorker(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) {
   182  	ready()
   183  
   184  	backoff := checkMachineAccountRetryBackoff()
   185  
   186  	for {
   187  		select {
   188  		case <-ctx.Done():
   189  			return
   190  		default:
   191  		}
   192  
   193  		err := validator.checkAndReportOnMachineAccountConfig(ctx)
   194  		if err != nil {
   195  			ctx.Throw(err)
   196  		}
   197  
   198  		next, _ := backoff.Next()
   199  		t := time.NewTimer(next)
   200  		select {
   201  		case <-ctx.Done():
   202  			t.Stop()
   203  			return
   204  		case <-t.C:
   205  		}
   206  	}
   207  }
   208  
   209  // checkAndReportOnMachineAccountConfig checks the node's machine account for misconfiguration
   210  // or insufficient balance once. Any discovered issues are logged and reported in metrics.
   211  // No errors are expected during normal operation.
   212  func (validator *MachineAccountConfigValidator) checkAndReportOnMachineAccountConfig(ctx context.Context) error {
   213  
   214  	account, err := validator.client.GetAccount(ctx, validator.info.SDKAddress())
   215  	if err != nil {
   216  		// we cannot validate a correct configuration - log an error and try again
   217  		validator.log.Error().
   218  			Err(err).
   219  			Str("machine_account_address", validator.info.Address).
   220  			Msg("failed to validate machine account config - could not get machine account")
   221  		return nil
   222  	}
   223  
   224  	accountBalance, err := ufix64Tofloat64(cadence.UFix64(account.Balance))
   225  	if err != nil {
   226  		return irrecoverable.NewExceptionf("failed to convert account balance (%d): %w", account.Balance, err)
   227  	}
   228  	validator.metrics.AccountBalance(accountBalance)
   229  
   230  	err = CheckMachineAccountInfo(validator.log, validator.config, validator.role, validator.info, account)
   231  	if err != nil {
   232  		// either we cannot validate the configuration or there is a critical
   233  		// misconfiguration - log a warning and retry - we will continue checking
   234  		// and logging until the problem is resolved
   235  		validator.metrics.IsMisconfigured(true)
   236  		validator.log.Error().
   237  			Err(err).
   238  			Msg("critical machine account misconfiguration")
   239  		return nil
   240  	}
   241  	validator.metrics.IsMisconfigured(false)
   242  
   243  	return nil
   244  }
   245  
   246  // CheckMachineAccountInfo checks a node machine account config, logging
   247  // anything noteworthy but not critical, and returning an error if the machine
   248  // account is not configured correctly, or the configuration cannot be checked.
   249  //
   250  // This function checks most aspects of correct configuration EXCEPT for
   251  // confirming that the account contains the relevant QCVoter or DKGParticipant
   252  // resource. This is omitted because it is not possible to query private account
   253  // info from a script.
   254  func CheckMachineAccountInfo(
   255  	log zerolog.Logger,
   256  	conf MachineAccountValidatorConfig,
   257  	role flow.Role,
   258  	info bootstrap.NodeMachineAccountInfo,
   259  	account *sdk.Account,
   260  ) error {
   261  
   262  	log.Debug().
   263  		Str("machine_account_address", info.Address).
   264  		Str("role", role.String()).
   265  		Msg("checking machine account configuration...")
   266  
   267  	if role != flow.RoleCollection && role != flow.RoleConsensus {
   268  		return fmt.Errorf("invalid role (%s) must be one of [collection, consensus]", role.String())
   269  	}
   270  
   271  	address := info.FlowAddress()
   272  	if address == flow.EmptyAddress {
   273  		return fmt.Errorf("could not parse machine account address: %s", info.Address)
   274  	}
   275  
   276  	privKey, err := sdkcrypto.DecodePrivateKey(info.SigningAlgorithm, info.EncodedPrivateKey)
   277  	if err != nil {
   278  		return fmt.Errorf("could not decode machine account private key: %w", err)
   279  	}
   280  
   281  	// FIRST - check the local account info independently
   282  	if info.HashAlgorithm != bootstrap.DefaultMachineAccountHashAlgo {
   283  		log.Warn().Msgf("non-standard hash algo (expected %s, got %s)", bootstrap.DefaultMachineAccountHashAlgo, info.HashAlgorithm.String())
   284  	}
   285  	if info.SigningAlgorithm != bootstrap.DefaultMachineAccountSignAlgo {
   286  		log.Warn().Msgf("non-standard signing algo (expected %s, got %s)", bootstrap.DefaultMachineAccountSignAlgo, info.SigningAlgorithm.String())
   287  	}
   288  	if info.KeyIndex != bootstrap.DefaultMachineAccountKeyIndex {
   289  		log.Warn().Msgf("non-standard key index (expected %d, got %d)", bootstrap.DefaultMachineAccountKeyIndex, info.KeyIndex)
   290  	}
   291  
   292  	// SECOND - compare the local account info to the on-chain account
   293  	if !bytes.Equal(account.Address.Bytes(), address.Bytes()) {
   294  		return fmt.Errorf("machine account address mismatch between local (%s) and on-chain (%s)", address, account.Address)
   295  	}
   296  	if len(account.Keys) <= int(info.KeyIndex) {
   297  		return fmt.Errorf("machine account (%s) has %d keys - but configured with key index %d", account.Address, len(account.Keys), info.KeyIndex)
   298  	}
   299  	accountKey := account.Keys[info.KeyIndex]
   300  	if accountKey.HashAlgo != info.HashAlgorithm {
   301  		return fmt.Errorf("machine account hash algo mismatch between local (%s) and on-chain (%s)",
   302  			info.HashAlgorithm.String(),
   303  			accountKey.HashAlgo.String())
   304  	}
   305  	if accountKey.SigAlgo != info.SigningAlgorithm {
   306  		return fmt.Errorf("machine account signing algo mismatch between local (%s) and on-chain (%s)",
   307  			info.SigningAlgorithm.String(),
   308  			accountKey.SigAlgo.String())
   309  	}
   310  	if accountKey.Index != int(info.KeyIndex) {
   311  		return fmt.Errorf("machine account key index mismatch between local (%d) and on-chain (%d)",
   312  			info.KeyIndex,
   313  			accountKey.Index)
   314  	}
   315  	if !accountKey.PublicKey.Equals(privKey.PublicKey()) {
   316  		return fmt.Errorf("machine account public key mismatch between local and on-chain")
   317  	}
   318  
   319  	// THIRD - check that the balance is sufficient
   320  	balance := cadence.UFix64(account.Balance)
   321  	log.Debug().Msgf("machine account balance: %s", balance.String())
   322  
   323  	switch role {
   324  	case flow.RoleCollection:
   325  		if balance < conf.HardMinBalanceLN {
   326  			return fmt.Errorf("machine account balance is below hard minimum (%s < %s)", balance, conf.HardMinBalanceLN)
   327  		}
   328  		if balance < conf.SoftMinBalanceLN {
   329  			log.Warn().Msgf("machine account balance is below recommended balance (%s < %s)", balance, conf.SoftMinBalanceLN)
   330  		}
   331  	case flow.RoleConsensus:
   332  		if balance < conf.HardMinBalanceSN {
   333  			return fmt.Errorf("machine account balance is below hard minimum (%s < %s)", balance, conf.HardMinBalanceSN)
   334  		}
   335  		if balance < conf.SoftMinBalanceSN {
   336  			log.Warn().Msgf("machine account balance is below recommended balance (%s < %s)", balance, conf.SoftMinBalanceSN)
   337  		}
   338  	default:
   339  		// sanity check - should be caught earlier in this function
   340  		return fmt.Errorf("invalid role (%s), must be collection or consensus", role)
   341  	}
   342  
   343  	return nil
   344  }
   345  
   346  // ufix64Tofloat64 converts a cadence.UFix64 type to float64.
   347  // All UFix64 values should be convertible to float64, so no errors are expected.
   348  func ufix64Tofloat64(fix cadence.UFix64) (float64, error) {
   349  	f, err := strconv.ParseFloat(fix.String(), 64)
   350  	if err != nil {
   351  		return 0, err
   352  	}
   353  	return f, nil
   354  }