github.com/koko1123/flow-go-1@v0.29.6/module/epochs/machine_account.go (about)

     1  package epochs
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"time"
     8  
     9  	"github.com/onflow/cadence"
    10  	"github.com/rs/zerolog"
    11  	"github.com/sethvargo/go-retry"
    12  
    13  	sdk "github.com/onflow/flow-go-sdk"
    14  	client "github.com/onflow/flow-go-sdk/access/grpc"
    15  	sdkcrypto "github.com/onflow/flow-go-sdk/crypto"
    16  	"github.com/koko1123/flow-go-1/engine"
    17  	"github.com/koko1123/flow-go-1/model/bootstrap"
    18  	"github.com/koko1123/flow-go-1/model/flow"
    19  )
    20  
    21  var (
    22  	// Hard and soft balance limits for collection and consensus nodes.
    23  	// We will log a warning once for a soft limit, and will log an error
    24  	// in perpetuity for a hard limit.
    25  	// Taken from https://www.notion.so/dapperlabs/Machine-Account-f3c293593ea442a39614fcebf705a132
    26  
    27  	defaultSoftMinBalanceLN cadence.UFix64
    28  	defaultHardMinBalanceLN cadence.UFix64
    29  	defaultSoftMinBalanceSN cadence.UFix64
    30  	defaultHardMinBalanceSN cadence.UFix64
    31  )
    32  
    33  func init() {
    34  	var err error
    35  	defaultSoftMinBalanceLN, err = cadence.NewUFix64("0.0025")
    36  	if err != nil {
    37  		panic(fmt.Errorf("could not convert soft min balance for LN: %w", err))
    38  	}
    39  	defaultHardMinBalanceLN, err = cadence.NewUFix64("0.002")
    40  	if err != nil {
    41  		panic(fmt.Errorf("could not convert hard min balance for LN: %w", err))
    42  	}
    43  	defaultSoftMinBalanceSN, err = cadence.NewUFix64("0.125")
    44  	if err != nil {
    45  		panic(fmt.Errorf("could not convert soft min balance for SN: %w", err))
    46  	}
    47  	defaultHardMinBalanceSN, err = cadence.NewUFix64("0.05")
    48  	if err != nil {
    49  		panic(fmt.Errorf("could not convert hard min balance for SN: %w", err))
    50  	}
    51  }
    52  
    53  const (
    54  	checkMachineAccountRetryBase      = time.Second * 5
    55  	checkMachineAccountRetryMax       = time.Minute * 10
    56  	checkMachineAccountRetryJitterPct = 5
    57  )
    58  
    59  // checkMachineAccountRetryBackoff returns the default backoff for checking
    60  // machine account configs.
    61  // * exponential backoff with base of 5s
    62  // * maximum inter-check wait of 10m
    63  // * 5% jitter
    64  func checkMachineAccountRetryBackoff() retry.Backoff {
    65  	backoff := retry.NewExponential(checkMachineAccountRetryBase)
    66  	backoff = retry.WithCappedDuration(checkMachineAccountRetryMax, backoff)
    67  	backoff = retry.WithJitterPercent(checkMachineAccountRetryJitterPct, backoff)
    68  	return backoff
    69  }
    70  
    71  // MachineAccountValidatorConfig defines configuration options for MachineAccountConfigValidator.
    72  type MachineAccountValidatorConfig struct {
    73  	SoftMinBalanceLN cadence.UFix64
    74  	HardMinBalanceLN cadence.UFix64
    75  	SoftMinBalanceSN cadence.UFix64
    76  	HardMinBalanceSN cadence.UFix64
    77  }
    78  
    79  func DefaultMachineAccountValidatorConfig() MachineAccountValidatorConfig {
    80  	return MachineAccountValidatorConfig{
    81  		SoftMinBalanceLN: defaultSoftMinBalanceLN,
    82  		HardMinBalanceLN: defaultHardMinBalanceLN,
    83  		SoftMinBalanceSN: defaultSoftMinBalanceSN,
    84  		HardMinBalanceSN: defaultHardMinBalanceSN,
    85  	}
    86  }
    87  
    88  // WithoutBalanceChecks sets minimum balances to 0 to effectively disable minimum
    89  // balance checks. This is useful for test networks where transaction fees are
    90  // disabled.
    91  func WithoutBalanceChecks(conf *MachineAccountValidatorConfig) {
    92  	conf.SoftMinBalanceLN = 0
    93  	conf.HardMinBalanceLN = 0
    94  	conf.SoftMinBalanceSN = 0
    95  	conf.HardMinBalanceSN = 0
    96  }
    97  
    98  type MachineAccountValidatorConfigOption func(*MachineAccountValidatorConfig)
    99  
   100  // MachineAccountConfigValidator is used to validate that a machine account is
   101  // configured correctly.
   102  type MachineAccountConfigValidator struct {
   103  	unit   *engine.Unit
   104  	config MachineAccountValidatorConfig
   105  	log    zerolog.Logger
   106  	client *client.Client
   107  	role   flow.Role
   108  	info   bootstrap.NodeMachineAccountInfo
   109  }
   110  
   111  func NewMachineAccountConfigValidator(
   112  	log zerolog.Logger,
   113  	flowClient *client.Client,
   114  	role flow.Role,
   115  	info bootstrap.NodeMachineAccountInfo,
   116  	opts ...MachineAccountValidatorConfigOption,
   117  ) (*MachineAccountConfigValidator, error) {
   118  
   119  	conf := DefaultMachineAccountValidatorConfig()
   120  	for _, apply := range opts {
   121  		apply(&conf)
   122  	}
   123  
   124  	validator := &MachineAccountConfigValidator{
   125  		unit:   engine.NewUnit(),
   126  		config: conf,
   127  		log:    log.With().Str("component", "machine_account_config_validator").Logger(),
   128  		client: flowClient,
   129  		role:   role,
   130  		info:   info,
   131  	}
   132  	return validator, nil
   133  }
   134  
   135  // Ready will launch the validator function in a goroutine.
   136  func (validator *MachineAccountConfigValidator) Ready() <-chan struct{} {
   137  	return validator.unit.Ready(func() {
   138  		validator.unit.Launch(func() {
   139  			validator.validateMachineAccountConfig(validator.unit.Ctx())
   140  		})
   141  	})
   142  }
   143  
   144  // Done will cancel the context of the unit, which will end the validator
   145  // goroutine, if it is still running.
   146  func (validator *MachineAccountConfigValidator) Done() <-chan struct{} {
   147  	return validator.unit.Done()
   148  }
   149  
   150  // validateMachineAccountConfig checks that the machine account in use by this
   151  // BaseClient object is correctly configured. If the machine account is critically
   152  // mis-configured, or a correct configuration cannot be confirmed, this function
   153  // will perpetually log errors indicating the problem.
   154  //
   155  // This function should be invoked as a goroutine by using Ready and Done.
   156  func (validator *MachineAccountConfigValidator) validateMachineAccountConfig(ctx context.Context) {
   157  
   158  	log := validator.log
   159  
   160  	backoff := checkMachineAccountRetryBackoff()
   161  
   162  	err := retry.Do(ctx, backoff, func(ctx context.Context) error {
   163  		account, err := validator.client.GetAccount(ctx, validator.info.SDKAddress())
   164  		if err != nil {
   165  			// we cannot validate a correct configuration - log an error and try again
   166  			log.Error().
   167  				Err(err).
   168  				Str("machine_account_address", validator.info.Address).
   169  				Msg("failed to validate machine account config - could not get machine account")
   170  			return retry.RetryableError(err)
   171  		}
   172  
   173  		err = CheckMachineAccountInfo(log, validator.config, validator.role, validator.info, account)
   174  		if err != nil {
   175  			// either we cannot validate the configuration or there is a critical
   176  			// misconfiguration - log a warning and retry - we will continue checking
   177  			// and logging until the problem is resolved
   178  			log.Error().
   179  				Err(err).
   180  				Msg("critical machine account misconfiguration")
   181  			return retry.RetryableError(err)
   182  		}
   183  		return nil
   184  	})
   185  	if err != nil {
   186  		log.Error().Err(err).Msg("failed to check machine account configuration after retry")
   187  		return
   188  	}
   189  
   190  	log.Info().Msg("confirmed valid machine account configuration. machine account config validator exiting...")
   191  }
   192  
   193  // CheckMachineAccountInfo checks a node machine account config, logging
   194  // anything noteworthy but not critical, and returning an error if the machine
   195  // account is not configured correctly, or the configuration cannot be checked.
   196  //
   197  // This function checks most aspects of correct configuration EXCEPT for
   198  // confirming that the account contains the relevant QCVoter or DKGParticipant
   199  // resource. This is omitted because it is not possible to query private account
   200  // info from a script.
   201  func CheckMachineAccountInfo(
   202  	log zerolog.Logger,
   203  	conf MachineAccountValidatorConfig,
   204  	role flow.Role,
   205  	info bootstrap.NodeMachineAccountInfo,
   206  	account *sdk.Account,
   207  ) error {
   208  
   209  	log.Debug().
   210  		Str("machine_account_address", info.Address).
   211  		Str("role", role.String()).
   212  		Msg("checking machine account configuration...")
   213  
   214  	if role != flow.RoleCollection && role != flow.RoleConsensus {
   215  		return fmt.Errorf("invalid role (%s) must be one of [collection, consensus]", role.String())
   216  	}
   217  
   218  	address := info.FlowAddress()
   219  	if address == flow.EmptyAddress {
   220  		return fmt.Errorf("could not parse machine account address: %s", info.Address)
   221  	}
   222  
   223  	privKey, err := sdkcrypto.DecodePrivateKey(info.SigningAlgorithm, info.EncodedPrivateKey)
   224  	if err != nil {
   225  		return fmt.Errorf("could not decode machine account private key: %w", err)
   226  	}
   227  
   228  	// FIRST - check the local account info independently
   229  	if info.HashAlgorithm != bootstrap.DefaultMachineAccountHashAlgo {
   230  		log.Warn().Msgf("non-standard hash algo (expected %s, got %s)", bootstrap.DefaultMachineAccountHashAlgo, info.HashAlgorithm.String())
   231  	}
   232  	if info.SigningAlgorithm != bootstrap.DefaultMachineAccountSignAlgo {
   233  		log.Warn().Msgf("non-standard signing algo (expected %s, got %s)", bootstrap.DefaultMachineAccountSignAlgo, info.SigningAlgorithm.String())
   234  	}
   235  	if info.KeyIndex != bootstrap.DefaultMachineAccountKeyIndex {
   236  		log.Warn().Msgf("non-standard key index (expected %d, got %d)", bootstrap.DefaultMachineAccountKeyIndex, info.KeyIndex)
   237  	}
   238  
   239  	// SECOND - compare the local account info to the on-chain account
   240  	if !bytes.Equal(account.Address.Bytes(), address.Bytes()) {
   241  		return fmt.Errorf("machine account address mismatch between local (%s) and on-chain (%s)", address, account.Address)
   242  	}
   243  	if len(account.Keys) <= int(info.KeyIndex) {
   244  		return fmt.Errorf("machine account (%s) has %d keys - but configured with key index %d", account.Address, len(account.Keys), info.KeyIndex)
   245  	}
   246  	accountKey := account.Keys[info.KeyIndex]
   247  	if accountKey.HashAlgo != info.HashAlgorithm {
   248  		return fmt.Errorf("machine account hash algo mismatch between local (%s) and on-chain (%s)",
   249  			info.HashAlgorithm.String(),
   250  			accountKey.HashAlgo.String())
   251  	}
   252  	if accountKey.SigAlgo != info.SigningAlgorithm {
   253  		return fmt.Errorf("machine account signing algo mismatch between local (%s) and on-chain (%s)",
   254  			info.SigningAlgorithm.String(),
   255  			accountKey.SigAlgo.String())
   256  	}
   257  	if accountKey.Index != int(info.KeyIndex) {
   258  		return fmt.Errorf("machine account key index mismatch between local (%d) and on-chain (%d)",
   259  			info.KeyIndex,
   260  			accountKey.Index)
   261  	}
   262  	if !accountKey.PublicKey.Equals(privKey.PublicKey()) {
   263  		return fmt.Errorf("machine account public key mismatch between local and on-chain")
   264  	}
   265  
   266  	// THIRD - check that the balance is sufficient
   267  	balance := cadence.UFix64(account.Balance)
   268  	log.Debug().Msgf("machine account balance: %s", balance.String())
   269  
   270  	switch role {
   271  	case flow.RoleCollection:
   272  		if balance < conf.HardMinBalanceLN {
   273  			return fmt.Errorf("machine account balance is below hard minimum (%s < %s)", balance, conf.HardMinBalanceLN)
   274  		}
   275  		if balance < conf.SoftMinBalanceLN {
   276  			log.Warn().Msgf("machine account balance is below recommended balance (%s < %s)", balance, conf.SoftMinBalanceLN)
   277  		}
   278  	case flow.RoleConsensus:
   279  		if balance < conf.HardMinBalanceSN {
   280  			return fmt.Errorf("machine account balance is below hard minimum (%s < %s)", balance, conf.HardMinBalanceSN)
   281  		}
   282  		if balance < conf.SoftMinBalanceSN {
   283  			log.Warn().Msgf("machine account balance is below recommended balance (%s < %s)", balance, conf.SoftMinBalanceSN)
   284  		}
   285  	default:
   286  		// sanity check - should be caught earlier in this function
   287  		return fmt.Errorf("invalid role (%s), must be collection or consensus", role)
   288  	}
   289  
   290  	return nil
   291  }