github.com/MetalBlockchain/metalgo@v1.11.9/vms/platformvm/block/builder/builder.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package builder 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "sync" 11 "time" 12 13 "go.uber.org/zap" 14 15 "github.com/MetalBlockchain/metalgo/ids" 16 "github.com/MetalBlockchain/metalgo/snow" 17 "github.com/MetalBlockchain/metalgo/snow/consensus/snowman" 18 "github.com/MetalBlockchain/metalgo/utils/set" 19 "github.com/MetalBlockchain/metalgo/utils/timer/mockable" 20 "github.com/MetalBlockchain/metalgo/utils/units" 21 "github.com/MetalBlockchain/metalgo/vms/platformvm/block" 22 "github.com/MetalBlockchain/metalgo/vms/platformvm/state" 23 "github.com/MetalBlockchain/metalgo/vms/platformvm/status" 24 "github.com/MetalBlockchain/metalgo/vms/platformvm/txs" 25 "github.com/MetalBlockchain/metalgo/vms/platformvm/txs/mempool" 26 27 blockexecutor "github.com/MetalBlockchain/metalgo/vms/platformvm/block/executor" 28 txexecutor "github.com/MetalBlockchain/metalgo/vms/platformvm/txs/executor" 29 ) 30 31 // targetBlockSize is maximum number of transaction bytes to place into a 32 // StandardBlock 33 const targetBlockSize = 128 * units.KiB 34 35 var ( 36 _ Builder = (*builder)(nil) 37 38 ErrEndOfTime = errors.New("program time is suspiciously far in the future") 39 ErrNoPendingBlocks = errors.New("no pending blocks") 40 errMissingPreferredState = errors.New("missing preferred block state") 41 errCalculatingNextStakerTime = errors.New("failed calculating next staker time") 42 ) 43 44 type Builder interface { 45 mempool.Mempool 46 47 // StartBlockTimer starts to issue block creation requests to advance the 48 // chain timestamp. 49 StartBlockTimer() 50 51 // ResetBlockTimer forces the block timer to recalculate when it should 52 // advance the chain timestamp. 53 ResetBlockTimer() 54 55 // ShutdownBlockTimer stops block creation requests to advance the chain 56 // timestamp. 57 // 58 // Invariant: Assumes the context lock is held when calling. 59 ShutdownBlockTimer() 60 61 // BuildBlock can be called to attempt to create a new block 62 BuildBlock(context.Context) (snowman.Block, error) 63 64 // PackBlockTxs returns an array of txs that can fit into a valid block of 65 // size [targetBlockSize]. The returned txs are all verified against the 66 // preferred state. 67 // 68 // Note: This function does not call the consensus engine. 69 PackBlockTxs(targetBlockSize int) ([]*txs.Tx, error) 70 } 71 72 // builder implements a simple builder to convert txs into valid blocks 73 type builder struct { 74 mempool.Mempool 75 76 txExecutorBackend *txexecutor.Backend 77 blkManager blockexecutor.Manager 78 79 // resetTimer is used to signal that the block builder timer should update 80 // when it will trigger building of a block. 81 resetTimer chan struct{} 82 closed chan struct{} 83 closeOnce sync.Once 84 } 85 86 func New( 87 mempool mempool.Mempool, 88 txExecutorBackend *txexecutor.Backend, 89 blkManager blockexecutor.Manager, 90 ) Builder { 91 return &builder{ 92 Mempool: mempool, 93 txExecutorBackend: txExecutorBackend, 94 blkManager: blkManager, 95 resetTimer: make(chan struct{}, 1), 96 closed: make(chan struct{}), 97 } 98 } 99 100 func (b *builder) StartBlockTimer() { 101 go func() { 102 timer := time.NewTimer(0) 103 defer timer.Stop() 104 105 for { 106 // Invariant: The [timer] is not stopped. 107 select { 108 case <-timer.C: 109 case <-b.resetTimer: 110 if !timer.Stop() { 111 <-timer.C 112 } 113 case <-b.closed: 114 return 115 } 116 117 // Note: Because the context lock is not held here, it is possible 118 // that [ShutdownBlockTimer] is called concurrently with this 119 // execution. 120 for { 121 duration, err := b.durationToSleep() 122 if err != nil { 123 b.txExecutorBackend.Ctx.Log.Error("block builder encountered a fatal error", 124 zap.Error(err), 125 ) 126 return 127 } 128 129 if duration > 0 { 130 timer.Reset(duration) 131 break 132 } 133 134 // Block needs to be issued to advance time. 135 b.Mempool.RequestBuildBlock(true /*=emptyBlockPermitted*/) 136 137 // Invariant: ResetBlockTimer is guaranteed to be called after 138 // [durationToSleep] returns a value <= 0. This is because we 139 // are guaranteed to attempt to build block. After building a 140 // valid block, the chain will have its preference updated which 141 // may change the duration to sleep and trigger a timer reset. 142 select { 143 case <-b.resetTimer: 144 case <-b.closed: 145 return 146 } 147 } 148 } 149 }() 150 } 151 152 func (b *builder) durationToSleep() (time.Duration, error) { 153 // Grabbing the lock here enforces that this function is not called mid-way 154 // through modifying of the state. 155 b.txExecutorBackend.Ctx.Lock.Lock() 156 defer b.txExecutorBackend.Ctx.Lock.Unlock() 157 158 // If [ShutdownBlockTimer] was called, we want to exit the block timer 159 // goroutine. We check this with the context lock held because 160 // [ShutdownBlockTimer] is expected to only be called with the context lock 161 // held. 162 select { 163 case <-b.closed: 164 return 0, nil 165 default: 166 } 167 168 preferredID := b.blkManager.Preferred() 169 preferredState, ok := b.blkManager.GetState(preferredID) 170 if !ok { 171 return 0, fmt.Errorf("%w: %s", errMissingPreferredState, preferredID) 172 } 173 174 nextStakerChangeTime, err := state.GetNextStakerChangeTime(preferredState) 175 if err != nil { 176 return 0, fmt.Errorf("%w of %s: %w", errCalculatingNextStakerTime, preferredID, err) 177 } 178 179 now := b.txExecutorBackend.Clk.Time() 180 return nextStakerChangeTime.Sub(now), nil 181 } 182 183 func (b *builder) ResetBlockTimer() { 184 // Ensure that the timer will be reset at least once. 185 select { 186 case b.resetTimer <- struct{}{}: 187 default: 188 } 189 } 190 191 func (b *builder) ShutdownBlockTimer() { 192 b.closeOnce.Do(func() { 193 close(b.closed) 194 }) 195 } 196 197 // BuildBlock builds a block to be added to consensus. 198 // This method removes the transactions from the returned 199 // blocks from the mempool. 200 func (b *builder) BuildBlock(context.Context) (snowman.Block, error) { 201 // If there are still transactions in the mempool, then we need to 202 // re-trigger block building. 203 defer b.Mempool.RequestBuildBlock(false /*=emptyBlockPermitted*/) 204 205 b.txExecutorBackend.Ctx.Log.Debug("starting to attempt to build a block") 206 207 // Get the block to build on top of and retrieve the new block's context. 208 preferredID := b.blkManager.Preferred() 209 preferred, err := b.blkManager.GetBlock(preferredID) 210 if err != nil { 211 return nil, err 212 } 213 nextHeight := preferred.Height() + 1 214 preferredState, ok := b.blkManager.GetState(preferredID) 215 if !ok { 216 return nil, fmt.Errorf("%w: %s", state.ErrMissingParentState, preferredID) 217 } 218 219 timestamp, timeWasCapped, err := state.NextBlockTime(preferredState, b.txExecutorBackend.Clk) 220 if err != nil { 221 return nil, fmt.Errorf("could not calculate next staker change time: %w", err) 222 } 223 224 statelessBlk, err := buildBlock( 225 b, 226 preferredID, 227 nextHeight, 228 timestamp, 229 timeWasCapped, 230 preferredState, 231 ) 232 if err != nil { 233 return nil, err 234 } 235 236 return b.blkManager.NewBlock(statelessBlk), nil 237 } 238 239 func (b *builder) PackBlockTxs(targetBlockSize int) ([]*txs.Tx, error) { 240 preferredID := b.blkManager.Preferred() 241 preferredState, ok := b.blkManager.GetState(preferredID) 242 if !ok { 243 return nil, fmt.Errorf("%w: %s", errMissingPreferredState, preferredID) 244 } 245 246 return packBlockTxs( 247 preferredID, 248 preferredState, 249 b.Mempool, 250 b.txExecutorBackend, 251 b.blkManager, 252 b.txExecutorBackend.Clk.Time(), 253 targetBlockSize, 254 ) 255 } 256 257 // [timestamp] is min(max(now, parent timestamp), next staker change time) 258 func buildBlock( 259 builder *builder, 260 parentID ids.ID, 261 height uint64, 262 timestamp time.Time, 263 forceAdvanceTime bool, 264 parentState state.Chain, 265 ) (block.Block, error) { 266 blockTxs, err := packBlockTxs( 267 parentID, 268 parentState, 269 builder.Mempool, 270 builder.txExecutorBackend, 271 builder.blkManager, 272 timestamp, 273 targetBlockSize, 274 ) 275 if err != nil { 276 return nil, fmt.Errorf("failed to pack block txs: %w", err) 277 } 278 279 // Try rewarding stakers whose staking period ends at the new chain time. 280 // This is done first to prioritize advancing the timestamp as quickly as 281 // possible. 282 stakerTxID, shouldReward, err := getNextStakerToReward(timestamp, parentState) 283 if err != nil { 284 return nil, fmt.Errorf("could not find next staker to reward: %w", err) 285 } 286 if shouldReward { 287 rewardValidatorTx, err := NewRewardValidatorTx(builder.txExecutorBackend.Ctx, stakerTxID) 288 if err != nil { 289 return nil, fmt.Errorf("could not build tx to reward staker: %w", err) 290 } 291 292 return block.NewBanffProposalBlock( 293 timestamp, 294 parentID, 295 height, 296 rewardValidatorTx, 297 blockTxs, 298 ) 299 } 300 301 // If there is no reason to build a block, don't. 302 if len(blockTxs) == 0 && !forceAdvanceTime { 303 builder.txExecutorBackend.Ctx.Log.Debug("no pending txs to issue into a block") 304 return nil, ErrNoPendingBlocks 305 } 306 307 // Issue a block with as many transactions as possible. 308 return block.NewBanffStandardBlock( 309 timestamp, 310 parentID, 311 height, 312 blockTxs, 313 ) 314 } 315 316 func packBlockTxs( 317 parentID ids.ID, 318 parentState state.Chain, 319 mempool mempool.Mempool, 320 backend *txexecutor.Backend, 321 manager blockexecutor.Manager, 322 timestamp time.Time, 323 remainingSize int, 324 ) ([]*txs.Tx, error) { 325 stateDiff, err := state.NewDiffOn(parentState) 326 if err != nil { 327 return nil, err 328 } 329 330 if _, err := txexecutor.AdvanceTimeTo(backend, stateDiff, timestamp); err != nil { 331 return nil, err 332 } 333 334 var ( 335 blockTxs []*txs.Tx 336 inputs set.Set[ids.ID] 337 ) 338 339 for { 340 tx, exists := mempool.Peek() 341 if !exists { 342 break 343 } 344 txSize := len(tx.Bytes()) 345 if txSize > remainingSize { 346 break 347 } 348 mempool.Remove(tx) 349 350 // Invariant: [tx] has already been syntactically verified. 351 352 txDiff, err := state.NewDiffOn(stateDiff) 353 if err != nil { 354 return nil, err 355 } 356 357 executor := &txexecutor.StandardTxExecutor{ 358 Backend: backend, 359 State: txDiff, 360 Tx: tx, 361 } 362 363 err = tx.Unsigned.Visit(executor) 364 if err != nil { 365 txID := tx.ID() 366 mempool.MarkDropped(txID, err) 367 continue 368 } 369 370 if inputs.Overlaps(executor.Inputs) { 371 txID := tx.ID() 372 mempool.MarkDropped(txID, blockexecutor.ErrConflictingBlockTxs) 373 continue 374 } 375 err = manager.VerifyUniqueInputs(parentID, executor.Inputs) 376 if err != nil { 377 txID := tx.ID() 378 mempool.MarkDropped(txID, err) 379 continue 380 } 381 inputs.Union(executor.Inputs) 382 383 txDiff.AddTx(tx, status.Committed) 384 err = txDiff.Apply(stateDiff) 385 if err != nil { 386 return nil, err 387 } 388 389 remainingSize -= txSize 390 blockTxs = append(blockTxs, tx) 391 } 392 393 return blockTxs, nil 394 } 395 396 // getNextStakerToReward returns the next staker txID to remove from the staking 397 // set with a RewardValidatorTx rather than an AdvanceTimeTx. [chainTimestamp] 398 // is the timestamp of the chain at the time this validator would be getting 399 // removed and is used to calculate [shouldReward]. 400 // Returns: 401 // - [txID] of the next staker to reward 402 // - [shouldReward] if the txID exists and is ready to be rewarded 403 // - [err] if something bad happened 404 func getNextStakerToReward( 405 chainTimestamp time.Time, 406 preferredState state.Chain, 407 ) (ids.ID, bool, error) { 408 if !chainTimestamp.Before(mockable.MaxTime) { 409 return ids.Empty, false, ErrEndOfTime 410 } 411 412 currentStakerIterator, err := preferredState.GetCurrentStakerIterator() 413 if err != nil { 414 return ids.Empty, false, err 415 } 416 defer currentStakerIterator.Release() 417 418 for currentStakerIterator.Next() { 419 currentStaker := currentStakerIterator.Value() 420 priority := currentStaker.Priority 421 // If the staker is a permissionless staker (not a permissioned subnet 422 // validator), it's the next staker we will want to remove with a 423 // RewardValidatorTx rather than an AdvanceTimeTx. 424 if priority != txs.SubnetPermissionedValidatorCurrentPriority { 425 return currentStaker.TxID, chainTimestamp.Equal(currentStaker.EndTime), nil 426 } 427 } 428 return ids.Empty, false, nil 429 } 430 431 func NewRewardValidatorTx(ctx *snow.Context, txID ids.ID) (*txs.Tx, error) { 432 utx := &txs.RewardValidatorTx{TxID: txID} 433 tx, err := txs.NewSigned(utx, txs.Codec, nil) 434 if err != nil { 435 return nil, err 436 } 437 return tx, tx.SyntacticVerify(ctx) 438 }