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 }