github.com/MetalBlockchain/metalgo@v1.11.9/vms/platformvm/validators/manager.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package validators 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "time" 11 12 "github.com/MetalBlockchain/metalgo/cache" 13 "github.com/MetalBlockchain/metalgo/ids" 14 "github.com/MetalBlockchain/metalgo/snow/validators" 15 "github.com/MetalBlockchain/metalgo/utils/constants" 16 "github.com/MetalBlockchain/metalgo/utils/logging" 17 "github.com/MetalBlockchain/metalgo/utils/timer/mockable" 18 "github.com/MetalBlockchain/metalgo/utils/window" 19 "github.com/MetalBlockchain/metalgo/vms/platformvm/block" 20 "github.com/MetalBlockchain/metalgo/vms/platformvm/config" 21 "github.com/MetalBlockchain/metalgo/vms/platformvm/metrics" 22 "github.com/MetalBlockchain/metalgo/vms/platformvm/status" 23 "github.com/MetalBlockchain/metalgo/vms/platformvm/txs" 24 ) 25 26 const ( 27 validatorSetsCacheSize = 64 28 maxRecentlyAcceptedWindowSize = 64 29 minRecentlyAcceptedWindowSize = 16 30 recentlyAcceptedWindowTTL = 2 * time.Minute 31 ) 32 33 var ( 34 _ validators.State = (*manager)(nil) 35 36 errUnfinalizedHeight = errors.New("failed to fetch validator set at unfinalized height") 37 ) 38 39 // Manager adds the ability to introduce newly accepted blocks IDs to the State 40 // interface. 41 type Manager interface { 42 validators.State 43 44 // OnAcceptedBlockID registers the ID of the latest accepted block. 45 // It is used to update the [recentlyAccepted] sliding window. 46 OnAcceptedBlockID(blkID ids.ID) 47 } 48 49 type State interface { 50 GetTx(txID ids.ID) (*txs.Tx, status.Status, error) 51 52 GetLastAccepted() ids.ID 53 GetStatelessBlock(blockID ids.ID) (block.Block, error) 54 55 // ApplyValidatorWeightDiffs iterates from [startHeight] towards the genesis 56 // block until it has applied all of the diffs up to and including 57 // [endHeight]. Applying the diffs modifies [validators]. 58 // 59 // Invariant: If attempting to generate the validator set for 60 // [endHeight - 1], [validators] must initially contain the validator 61 // weights for [startHeight]. 62 // 63 // Note: Because this function iterates towards the genesis, [startHeight] 64 // should normally be greater than or equal to [endHeight]. 65 ApplyValidatorWeightDiffs( 66 ctx context.Context, 67 validators map[ids.NodeID]*validators.GetValidatorOutput, 68 startHeight uint64, 69 endHeight uint64, 70 subnetID ids.ID, 71 ) error 72 73 // ApplyValidatorPublicKeyDiffs iterates from [startHeight] towards the 74 // genesis block until it has applied all of the diffs up to and including 75 // [endHeight]. Applying the diffs modifies [validators]. 76 // 77 // Invariant: If attempting to generate the validator set for 78 // [endHeight - 1], [validators] must initially contain the validator 79 // weights for [startHeight]. 80 // 81 // Note: Because this function iterates towards the genesis, [startHeight] 82 // should normally be greater than or equal to [endHeight]. 83 ApplyValidatorPublicKeyDiffs( 84 ctx context.Context, 85 validators map[ids.NodeID]*validators.GetValidatorOutput, 86 startHeight uint64, 87 endHeight uint64, 88 ) error 89 } 90 91 func NewManager( 92 log logging.Logger, 93 cfg config.Config, 94 state State, 95 metrics metrics.Metrics, 96 clk *mockable.Clock, 97 ) Manager { 98 return &manager{ 99 log: log, 100 cfg: cfg, 101 state: state, 102 metrics: metrics, 103 clk: clk, 104 caches: make(map[ids.ID]cache.Cacher[uint64, map[ids.NodeID]*validators.GetValidatorOutput]), 105 recentlyAccepted: window.New[ids.ID]( 106 window.Config{ 107 Clock: clk, 108 MaxSize: maxRecentlyAcceptedWindowSize, 109 MinSize: minRecentlyAcceptedWindowSize, 110 TTL: recentlyAcceptedWindowTTL, 111 }, 112 ), 113 } 114 } 115 116 // TODO: Remove requirement for the P-chain's context lock to be held when 117 // calling exported functions. 118 type manager struct { 119 log logging.Logger 120 cfg config.Config 121 state State 122 metrics metrics.Metrics 123 clk *mockable.Clock 124 125 // Maps caches for each subnet that is currently tracked. 126 // Key: Subnet ID 127 // Value: cache mapping height -> validator set map 128 caches map[ids.ID]cache.Cacher[uint64, map[ids.NodeID]*validators.GetValidatorOutput] 129 130 // sliding window of blocks that were recently accepted 131 recentlyAccepted window.Window[ids.ID] 132 } 133 134 // GetMinimumHeight returns the height of the most recent block beyond the 135 // horizon of our recentlyAccepted window. 136 // 137 // Because the time between blocks is arbitrary, we're only guaranteed that 138 // the window's configured TTL amount of time has passed once an element 139 // expires from the window. 140 // 141 // To try to always return a block older than the window's TTL, we return the 142 // parent of the oldest element in the window (as an expired element is always 143 // guaranteed to be sufficiently stale). If we haven't expired an element yet 144 // in the case of a process restart, we default to the lastAccepted block's 145 // height which is likely (but not guaranteed) to also be older than the 146 // window's configured TTL. 147 // 148 // If [UseCurrentHeight] is true, we override the block selection policy 149 // described above and we will always return the last accepted block height 150 // as the minimum. 151 func (m *manager) GetMinimumHeight(ctx context.Context) (uint64, error) { 152 if m.cfg.UseCurrentHeight { 153 return m.getCurrentHeight(ctx) 154 } 155 156 oldest, ok := m.recentlyAccepted.Oldest() 157 if !ok { 158 return m.getCurrentHeight(ctx) 159 } 160 161 blk, err := m.state.GetStatelessBlock(oldest) 162 if err != nil { 163 return 0, err 164 } 165 166 // We subtract 1 from the height of [oldest] because we want the height of 167 // the last block accepted before the [recentlyAccepted] window. 168 // 169 // There is guaranteed to be a block accepted before this window because the 170 // first block added to [recentlyAccepted] window is >= height 1. 171 return blk.Height() - 1, nil 172 } 173 174 func (m *manager) GetCurrentHeight(ctx context.Context) (uint64, error) { 175 return m.getCurrentHeight(ctx) 176 } 177 178 // TODO: Pass the context into the state. 179 func (m *manager) getCurrentHeight(context.Context) (uint64, error) { 180 lastAcceptedID := m.state.GetLastAccepted() 181 lastAccepted, err := m.state.GetStatelessBlock(lastAcceptedID) 182 if err != nil { 183 return 0, err 184 } 185 return lastAccepted.Height(), nil 186 } 187 188 func (m *manager) GetValidatorSet( 189 ctx context.Context, 190 targetHeight uint64, 191 subnetID ids.ID, 192 ) (map[ids.NodeID]*validators.GetValidatorOutput, error) { 193 validatorSetsCache := m.getValidatorSetCache(subnetID) 194 195 if validatorSet, ok := validatorSetsCache.Get(targetHeight); ok { 196 m.metrics.IncValidatorSetsCached() 197 return validatorSet, nil 198 } 199 200 // get the start time to track metrics 201 startTime := m.clk.Time() 202 203 var ( 204 validatorSet map[ids.NodeID]*validators.GetValidatorOutput 205 currentHeight uint64 206 err error 207 ) 208 if subnetID == constants.PrimaryNetworkID { 209 validatorSet, currentHeight, err = m.makePrimaryNetworkValidatorSet(ctx, targetHeight) 210 } else { 211 validatorSet, currentHeight, err = m.makeSubnetValidatorSet(ctx, targetHeight, subnetID) 212 } 213 if err != nil { 214 return nil, err 215 } 216 217 // cache the validator set 218 validatorSetsCache.Put(targetHeight, validatorSet) 219 220 duration := m.clk.Time().Sub(startTime) 221 m.metrics.IncValidatorSetsCreated() 222 m.metrics.AddValidatorSetsDuration(duration) 223 m.metrics.AddValidatorSetsHeightDiff(currentHeight - targetHeight) 224 return validatorSet, nil 225 } 226 227 func (m *manager) getValidatorSetCache(subnetID ids.ID) cache.Cacher[uint64, map[ids.NodeID]*validators.GetValidatorOutput] { 228 // Only cache tracked subnets 229 if subnetID != constants.PrimaryNetworkID && !m.cfg.TrackedSubnets.Contains(subnetID) { 230 return &cache.Empty[uint64, map[ids.NodeID]*validators.GetValidatorOutput]{} 231 } 232 233 validatorSetsCache, exists := m.caches[subnetID] 234 if exists { 235 return validatorSetsCache 236 } 237 238 validatorSetsCache = &cache.LRU[uint64, map[ids.NodeID]*validators.GetValidatorOutput]{ 239 Size: validatorSetsCacheSize, 240 } 241 m.caches[subnetID] = validatorSetsCache 242 return validatorSetsCache 243 } 244 245 func (m *manager) makePrimaryNetworkValidatorSet( 246 ctx context.Context, 247 targetHeight uint64, 248 ) (map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) { 249 validatorSet, currentHeight, err := m.getCurrentPrimaryValidatorSet(ctx) 250 if err != nil { 251 return nil, 0, err 252 } 253 if currentHeight < targetHeight { 254 return nil, 0, fmt.Errorf("%w with SubnetID = %s: current P-chain height (%d) < requested P-Chain height (%d)", 255 errUnfinalizedHeight, 256 constants.PrimaryNetworkID, 257 currentHeight, 258 targetHeight, 259 ) 260 } 261 262 // Rebuild primary network validators at [targetHeight] 263 // 264 // Note: Since we are attempting to generate the validator set at 265 // [targetHeight], we want to apply the diffs from 266 // (targetHeight, currentHeight]. Because the state interface is implemented 267 // to be inclusive, we apply diffs in [targetHeight + 1, currentHeight]. 268 lastDiffHeight := targetHeight + 1 269 err = m.state.ApplyValidatorWeightDiffs( 270 ctx, 271 validatorSet, 272 currentHeight, 273 lastDiffHeight, 274 constants.PlatformChainID, 275 ) 276 if err != nil { 277 return nil, 0, err 278 } 279 280 err = m.state.ApplyValidatorPublicKeyDiffs( 281 ctx, 282 validatorSet, 283 currentHeight, 284 lastDiffHeight, 285 ) 286 return validatorSet, currentHeight, err 287 } 288 289 func (m *manager) getCurrentPrimaryValidatorSet( 290 ctx context.Context, 291 ) (map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) { 292 primaryMap := m.cfg.Validators.GetMap(constants.PrimaryNetworkID) 293 currentHeight, err := m.getCurrentHeight(ctx) 294 return primaryMap, currentHeight, err 295 } 296 297 func (m *manager) makeSubnetValidatorSet( 298 ctx context.Context, 299 targetHeight uint64, 300 subnetID ids.ID, 301 ) (map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) { 302 subnetValidatorSet, primaryValidatorSet, currentHeight, err := m.getCurrentValidatorSets(ctx, subnetID) 303 if err != nil { 304 return nil, 0, err 305 } 306 if currentHeight < targetHeight { 307 return nil, 0, fmt.Errorf("%w with SubnetID = %s: current P-chain height (%d) < requested P-Chain height (%d)", 308 errUnfinalizedHeight, 309 subnetID, 310 currentHeight, 311 targetHeight, 312 ) 313 } 314 315 // Rebuild subnet validators at [targetHeight] 316 // 317 // Note: Since we are attempting to generate the validator set at 318 // [targetHeight], we want to apply the diffs from 319 // (targetHeight, currentHeight]. Because the state interface is implemented 320 // to be inclusive, we apply diffs in [targetHeight + 1, currentHeight]. 321 lastDiffHeight := targetHeight + 1 322 err = m.state.ApplyValidatorWeightDiffs( 323 ctx, 324 subnetValidatorSet, 325 currentHeight, 326 lastDiffHeight, 327 subnetID, 328 ) 329 if err != nil { 330 return nil, 0, err 331 } 332 333 // Update the subnet validator set to include the public keys at 334 // [currentHeight]. When we apply the public key diffs, we will convert 335 // these keys to represent the public keys at [targetHeight]. If the subnet 336 // validator is not currently a primary network validator, it doesn't have a 337 // key at [currentHeight]. 338 for nodeID, vdr := range subnetValidatorSet { 339 if primaryVdr, ok := primaryValidatorSet[nodeID]; ok { 340 vdr.PublicKey = primaryVdr.PublicKey 341 } else { 342 vdr.PublicKey = nil 343 } 344 } 345 346 err = m.state.ApplyValidatorPublicKeyDiffs( 347 ctx, 348 subnetValidatorSet, 349 currentHeight, 350 lastDiffHeight, 351 ) 352 return subnetValidatorSet, currentHeight, err 353 } 354 355 func (m *manager) getCurrentValidatorSets( 356 ctx context.Context, 357 subnetID ids.ID, 358 ) (map[ids.NodeID]*validators.GetValidatorOutput, map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) { 359 subnetMap := m.cfg.Validators.GetMap(subnetID) 360 primaryMap := m.cfg.Validators.GetMap(constants.PrimaryNetworkID) 361 currentHeight, err := m.getCurrentHeight(ctx) 362 return subnetMap, primaryMap, currentHeight, err 363 } 364 365 func (m *manager) GetSubnetID(_ context.Context, chainID ids.ID) (ids.ID, error) { 366 if chainID == constants.PlatformChainID { 367 return constants.PrimaryNetworkID, nil 368 } 369 370 chainTx, _, err := m.state.GetTx(chainID) 371 if err != nil { 372 return ids.Empty, fmt.Errorf( 373 "problem retrieving blockchain %q: %w", 374 chainID, 375 err, 376 ) 377 } 378 chain, ok := chainTx.Unsigned.(*txs.CreateChainTx) 379 if !ok { 380 return ids.Empty, fmt.Errorf("%q is not a blockchain", chainID) 381 } 382 return chain.SubnetID, nil 383 } 384 385 func (m *manager) OnAcceptedBlockID(blkID ids.ID) { 386 m.recentlyAccepted.Add(blkID) 387 }