github.com/ava-labs/avalanchego@v1.11.11/vms/proposervm/proposer/windower.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package proposer 5 6 import ( 7 "context" 8 "errors" 9 "math/bits" 10 "time" 11 12 "gonum.org/v1/gonum/mathext/prng" 13 14 "github.com/ava-labs/avalanchego/ids" 15 "github.com/ava-labs/avalanchego/snow/validators" 16 "github.com/ava-labs/avalanchego/utils" 17 "github.com/ava-labs/avalanchego/utils/math" 18 "github.com/ava-labs/avalanchego/utils/sampler" 19 "github.com/ava-labs/avalanchego/utils/wrappers" 20 ) 21 22 // Proposer list constants 23 const ( 24 WindowDuration = 5 * time.Second 25 26 MaxVerifyWindows = 6 27 MaxVerifyDelay = MaxVerifyWindows * WindowDuration // 30 seconds 28 29 MaxBuildWindows = 60 30 MaxBuildDelay = MaxBuildWindows * WindowDuration // 5 minutes 31 32 MaxLookAheadSlots = 720 33 MaxLookAheadWindow = MaxLookAheadSlots * WindowDuration // 1 hour 34 ) 35 36 var ( 37 _ Windower = (*windower)(nil) 38 39 ErrAnyoneCanPropose = errors.New("anyone can propose") 40 ErrUnexpectedSamplerFailure = errors.New("unexpected sampler failure") 41 ) 42 43 type Windower interface { 44 // Proposers returns the proposer list for building a block at [blockHeight] 45 // when the validator set is defined at [pChainHeight]. The list is returned 46 // in order. The minimum delay of a validator is the index they appear times 47 // [WindowDuration]. 48 Proposers( 49 ctx context.Context, 50 blockHeight, 51 pChainHeight uint64, 52 maxWindows int, 53 ) ([]ids.NodeID, error) 54 55 // Delay returns the amount of time that [validatorID] must wait before 56 // building a block at [blockHeight] when the validator set is defined at 57 // [pChainHeight]. 58 Delay( 59 ctx context.Context, 60 blockHeight, 61 pChainHeight uint64, 62 validatorID ids.NodeID, 63 maxWindows int, 64 ) (time.Duration, error) 65 66 // In the Post-Durango windowing scheme, every validator active at 67 // [pChainHeight] gets specific slots it can propose in (instead of being 68 // able to propose from a given time on as it happens Pre-Durango). 69 // [ExpectedProposer] calculates which nodeID is scheduled to propose a 70 // block of height [blockHeight] at [slot]. 71 // If no validators are currently available, [ErrAnyoneCanPropose] is 72 // returned. 73 ExpectedProposer( 74 ctx context.Context, 75 blockHeight, 76 pChainHeight, 77 slot uint64, 78 ) (ids.NodeID, error) 79 80 // In the Post-Durango windowing scheme, every validator active at 81 // [pChainHeight] gets specific slots it can propose in (instead of being 82 // able to propose from a given time on as it happens Pre-Durango). 83 // [MinDelayForProposer] specifies how long [nodeID] needs to wait for its 84 // slot to start. Delay is specified as starting from slot zero start. 85 // (which is parent timestamp). For efficiency reasons, we cap the slot 86 // search to [MaxLookAheadSlots]. 87 // If no validators are currently available, [ErrAnyoneCanPropose] is 88 // returned. 89 MinDelayForProposer( 90 ctx context.Context, 91 blockHeight, 92 pChainHeight uint64, 93 nodeID ids.NodeID, 94 startSlot uint64, 95 ) (time.Duration, error) 96 } 97 98 // windower interfaces with P-Chain and it is responsible for calculating the 99 // delay for the block submission window of a given validator 100 type windower struct { 101 state validators.State 102 subnetID ids.ID 103 chainSource uint64 104 } 105 106 func New(state validators.State, subnetID, chainID ids.ID) Windower { 107 w := wrappers.Packer{Bytes: chainID[:]} 108 return &windower{ 109 state: state, 110 subnetID: subnetID, 111 chainSource: w.UnpackLong(), 112 } 113 } 114 115 func (w *windower) Proposers(ctx context.Context, blockHeight, pChainHeight uint64, maxWindows int) ([]ids.NodeID, error) { 116 // Note: The 32-bit prng is used here for legacy reasons. All other usages 117 // of a prng in this file should use the 64-bit version. 118 source := prng.NewMT19937() 119 sampler, validators, err := w.makeSampler(ctx, pChainHeight, source) 120 if err != nil { 121 return nil, err 122 } 123 124 var totalWeight uint64 125 for _, validator := range validators { 126 totalWeight, err = math.Add(totalWeight, validator.weight) 127 if err != nil { 128 return nil, err 129 } 130 } 131 132 source.Seed(w.chainSource ^ blockHeight) 133 134 numToSample := int(min(uint64(maxWindows), totalWeight)) 135 indices, ok := sampler.Sample(numToSample) 136 if !ok { 137 return nil, ErrUnexpectedSamplerFailure 138 } 139 140 nodeIDs := make([]ids.NodeID, numToSample) 141 for i, index := range indices { 142 nodeIDs[i] = validators[index].id 143 } 144 return nodeIDs, nil 145 } 146 147 func (w *windower) Delay(ctx context.Context, blockHeight, pChainHeight uint64, validatorID ids.NodeID, maxWindows int) (time.Duration, error) { 148 if validatorID == ids.EmptyNodeID { 149 return time.Duration(maxWindows) * WindowDuration, nil 150 } 151 152 proposers, err := w.Proposers(ctx, blockHeight, pChainHeight, maxWindows) 153 if err != nil { 154 return 0, err 155 } 156 157 delay := time.Duration(0) 158 for _, nodeID := range proposers { 159 if nodeID == validatorID { 160 return delay, nil 161 } 162 delay += WindowDuration 163 } 164 return delay, nil 165 } 166 167 func (w *windower) ExpectedProposer( 168 ctx context.Context, 169 blockHeight, 170 pChainHeight, 171 slot uint64, 172 ) (ids.NodeID, error) { 173 source := prng.NewMT19937_64() 174 sampler, validators, err := w.makeSampler(ctx, pChainHeight, source) 175 if err != nil { 176 return ids.EmptyNodeID, err 177 } 178 if len(validators) == 0 { 179 return ids.EmptyNodeID, ErrAnyoneCanPropose 180 } 181 182 return w.expectedProposer( 183 validators, 184 source, 185 sampler, 186 blockHeight, 187 slot, 188 ) 189 } 190 191 func (w *windower) MinDelayForProposer( 192 ctx context.Context, 193 blockHeight, 194 pChainHeight uint64, 195 nodeID ids.NodeID, 196 startSlot uint64, 197 ) (time.Duration, error) { 198 source := prng.NewMT19937_64() 199 sampler, validators, err := w.makeSampler(ctx, pChainHeight, source) 200 if err != nil { 201 return 0, err 202 } 203 if len(validators) == 0 { 204 return 0, ErrAnyoneCanPropose 205 } 206 207 maxSlot := startSlot + MaxLookAheadSlots 208 for slot := startSlot; slot < maxSlot; slot++ { 209 expectedNodeID, err := w.expectedProposer( 210 validators, 211 source, 212 sampler, 213 blockHeight, 214 slot, 215 ) 216 if err != nil { 217 return 0, err 218 } 219 220 if expectedNodeID == nodeID { 221 return time.Duration(slot) * WindowDuration, nil 222 } 223 } 224 225 // no slots scheduled for the max window we inspect. Return max delay 226 return time.Duration(maxSlot) * WindowDuration, nil 227 } 228 229 func (w *windower) makeSampler( 230 ctx context.Context, 231 pChainHeight uint64, 232 source sampler.Source, 233 ) (sampler.WeightedWithoutReplacement, []validatorData, error) { 234 // Get the canonical representation of the validator set at the provided 235 // p-chain height. 236 validatorsMap, err := w.state.GetValidatorSet(ctx, pChainHeight, w.subnetID) 237 if err != nil { 238 return nil, nil, err 239 } 240 241 validators := make([]validatorData, 0, len(validatorsMap)) 242 for k, v := range validatorsMap { 243 validators = append(validators, validatorData{ 244 id: k, 245 weight: v.Weight, 246 }) 247 } 248 249 // Note: validators are sorted by ID. Sorting by weight would not create a 250 // canonically sorted list. 251 utils.Sort(validators) 252 253 weights := make([]uint64, len(validators)) 254 for i, validator := range validators { 255 weights[i] = validator.weight 256 } 257 258 sampler := sampler.NewDeterministicWeightedWithoutReplacement(source) 259 return sampler, validators, sampler.Initialize(weights) 260 } 261 262 func (w *windower) expectedProposer( 263 validators []validatorData, 264 source *prng.MT19937_64, 265 sampler sampler.WeightedWithoutReplacement, 266 blockHeight, 267 slot uint64, 268 ) (ids.NodeID, error) { 269 // Slot is reversed to utilize a different state space in the seed than the 270 // height. If the slot was not reversed the state space would collide; 271 // biasing the seed generation. For example, without reversing the slot 272 // height=0 and slot=1 would equal height=1 and slot=0. 273 source.Seed(w.chainSource ^ blockHeight ^ bits.Reverse64(slot)) 274 indices, ok := sampler.Sample(1) 275 if !ok { 276 return ids.EmptyNodeID, ErrUnexpectedSamplerFailure 277 } 278 return validators[indices[0]].id, nil 279 } 280 281 func TimeToSlot(start, now time.Time) uint64 { 282 if now.Before(start) { 283 return 0 284 } 285 return uint64(now.Sub(start) / WindowDuration) 286 }