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 }