github.com/ava-labs/avalanchego@v1.11.11/vms/proposervm/block.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package proposervm 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "time" 11 12 "go.uber.org/zap" 13 14 "github.com/ava-labs/avalanchego/ids" 15 "github.com/ava-labs/avalanchego/snow" 16 "github.com/ava-labs/avalanchego/snow/consensus/snowman" 17 "github.com/ava-labs/avalanchego/vms/proposervm/block" 18 "github.com/ava-labs/avalanchego/vms/proposervm/proposer" 19 20 smblock "github.com/ava-labs/avalanchego/snow/engine/snowman/block" 21 ) 22 23 const ( 24 // allowable block issuance in the future 25 maxSkew = 10 * time.Second 26 ) 27 28 var ( 29 errUnsignedChild = errors.New("expected child to be signed") 30 errUnexpectedBlockType = errors.New("unexpected proposer block type") 31 errInnerParentMismatch = errors.New("inner parentID didn't match expected parent") 32 errTimeNotMonotonic = errors.New("time must monotonically increase") 33 errPChainHeightNotMonotonic = errors.New("non monotonically increasing P-chain height") 34 errPChainHeightNotReached = errors.New("block P-chain height larger than current P-chain height") 35 errTimeTooAdvanced = errors.New("time is too far advanced") 36 errProposerWindowNotStarted = errors.New("proposer window hasn't started") 37 errUnexpectedProposer = errors.New("unexpected proposer for current window") 38 errProposerMismatch = errors.New("proposer mismatch") 39 errProposersNotActivated = errors.New("proposers haven't been activated yet") 40 errPChainHeightTooLow = errors.New("block P-chain height is too low") 41 ) 42 43 type Block interface { 44 snowman.Block 45 46 getInnerBlk() snowman.Block 47 48 // After a state sync, we may need to update last accepted block data 49 // without propagating any changes to the innerVM. 50 // acceptOuterBlk and acceptInnerBlk allow controlling acceptance of outer 51 // and inner blocks. 52 acceptOuterBlk() error 53 acceptInnerBlk(context.Context) error 54 55 verifyPreForkChild(ctx context.Context, child *preForkBlock) error 56 verifyPostForkChild(ctx context.Context, child *postForkBlock) error 57 verifyPostForkOption(ctx context.Context, child *postForkOption) error 58 59 buildChild(context.Context) (Block, error) 60 61 pChainHeight(context.Context) (uint64, error) 62 } 63 64 type PostForkBlock interface { 65 Block 66 67 getStatelessBlk() block.Block 68 setInnerBlk(snowman.Block) 69 } 70 71 // field of postForkBlock and postForkOption 72 type postForkCommonComponents struct { 73 vm *VM 74 innerBlk snowman.Block 75 } 76 77 // Return the inner block's height 78 func (p *postForkCommonComponents) Height() uint64 { 79 return p.innerBlk.Height() 80 } 81 82 // Verify returns nil if: 83 // 1) [p]'s inner block is not an oracle block 84 // 2) [child]'s P-Chain height >= [parentPChainHeight] 85 // 3) [p]'s inner block is the parent of [c]'s inner block 86 // 4) [child]'s timestamp isn't before [p]'s timestamp 87 // 5) [child]'s timestamp is within the skew bound 88 // 6) [childPChainHeight] <= the current P-Chain height 89 // 7) [child]'s timestamp is within its proposer's window 90 // 8) [child] has a valid signature from its proposer 91 // 9) [child]'s inner block is valid 92 func (p *postForkCommonComponents) Verify( 93 ctx context.Context, 94 parentTimestamp time.Time, 95 parentPChainHeight uint64, 96 child *postForkBlock, 97 ) error { 98 if err := verifyIsNotOracleBlock(ctx, p.innerBlk); err != nil { 99 return err 100 } 101 102 childPChainHeight := child.PChainHeight() 103 if childPChainHeight < parentPChainHeight { 104 return errPChainHeightNotMonotonic 105 } 106 107 expectedInnerParentID := p.innerBlk.ID() 108 innerParentID := child.innerBlk.Parent() 109 if innerParentID != expectedInnerParentID { 110 return errInnerParentMismatch 111 } 112 113 childTimestamp := child.Timestamp() 114 if childTimestamp.Before(parentTimestamp) { 115 return errTimeNotMonotonic 116 } 117 118 maxTimestamp := p.vm.Time().Add(maxSkew) 119 if childTimestamp.After(maxTimestamp) { 120 return errTimeTooAdvanced 121 } 122 123 // If the node is currently syncing - we don't assume that the P-chain has 124 // been synced up to this point yet. 125 if p.vm.consensusState == snow.NormalOp { 126 currentPChainHeight, err := p.vm.ctx.ValidatorState.GetCurrentHeight(ctx) 127 if err != nil { 128 p.vm.ctx.Log.Error("block verification failed", 129 zap.String("reason", "failed to get current P-Chain height"), 130 zap.Stringer("blkID", child.ID()), 131 zap.Error(err), 132 ) 133 return err 134 } 135 if childPChainHeight > currentPChainHeight { 136 return fmt.Errorf("%w: %d > %d", 137 errPChainHeightNotReached, 138 childPChainHeight, 139 currentPChainHeight, 140 ) 141 } 142 143 var shouldHaveProposer bool 144 if p.vm.Upgrades.IsDurangoActivated(parentTimestamp) { 145 shouldHaveProposer, err = p.verifyPostDurangoBlockDelay(ctx, parentTimestamp, parentPChainHeight, child) 146 } else { 147 shouldHaveProposer, err = p.verifyPreDurangoBlockDelay(ctx, parentTimestamp, parentPChainHeight, child) 148 } 149 if err != nil { 150 return err 151 } 152 153 hasProposer := child.SignedBlock.Proposer() != ids.EmptyNodeID 154 if shouldHaveProposer != hasProposer { 155 return fmt.Errorf("%w: shouldHaveProposer (%v) != hasProposer (%v)", errProposerMismatch, shouldHaveProposer, hasProposer) 156 } 157 158 p.vm.ctx.Log.Debug("verified post-fork block", 159 zap.Stringer("blkID", child.ID()), 160 zap.Time("parentTimestamp", parentTimestamp), 161 zap.Time("blockTimestamp", childTimestamp), 162 ) 163 } 164 165 return p.vm.verifyAndRecordInnerBlk( 166 ctx, 167 &smblock.Context{ 168 PChainHeight: parentPChainHeight, 169 }, 170 child, 171 ) 172 } 173 174 // Return the child (a *postForkBlock) of this block 175 func (p *postForkCommonComponents) buildChild( 176 ctx context.Context, 177 parentID ids.ID, 178 parentTimestamp time.Time, 179 parentPChainHeight uint64, 180 ) (Block, error) { 181 // Child's timestamp is the later of now and this block's timestamp 182 newTimestamp := p.vm.Time().Truncate(time.Second) 183 if newTimestamp.Before(parentTimestamp) { 184 newTimestamp = parentTimestamp 185 } 186 187 // The child's P-Chain height is proposed as the optimal P-Chain height that 188 // is at least the parent's P-Chain height 189 pChainHeight, err := p.vm.optimalPChainHeight(ctx, parentPChainHeight) 190 if err != nil { 191 p.vm.ctx.Log.Error("unexpected build block failure", 192 zap.String("reason", "failed to calculate optimal P-chain height"), 193 zap.Stringer("parentID", parentID), 194 zap.Error(err), 195 ) 196 return nil, err 197 } 198 199 var shouldBuildSignedBlock bool 200 if p.vm.Upgrades.IsDurangoActivated(parentTimestamp) { 201 shouldBuildSignedBlock, err = p.shouldBuildSignedBlockPostDurango( 202 ctx, 203 parentID, 204 parentTimestamp, 205 parentPChainHeight, 206 newTimestamp, 207 ) 208 } else { 209 shouldBuildSignedBlock, err = p.shouldBuildSignedBlockPreDurango( 210 ctx, 211 parentID, 212 parentTimestamp, 213 parentPChainHeight, 214 newTimestamp, 215 ) 216 } 217 if err != nil { 218 return nil, err 219 } 220 221 var innerBlock snowman.Block 222 if p.vm.blockBuilderVM != nil { 223 innerBlock, err = p.vm.blockBuilderVM.BuildBlockWithContext(ctx, &smblock.Context{ 224 PChainHeight: parentPChainHeight, 225 }) 226 } else { 227 innerBlock, err = p.vm.ChainVM.BuildBlock(ctx) 228 } 229 if err != nil { 230 return nil, err 231 } 232 233 // Build the child 234 var statelessChild block.SignedBlock 235 if shouldBuildSignedBlock { 236 statelessChild, err = block.Build( 237 parentID, 238 newTimestamp, 239 pChainHeight, 240 p.vm.StakingCertLeaf, 241 innerBlock.Bytes(), 242 p.vm.ctx.ChainID, 243 p.vm.StakingLeafSigner, 244 ) 245 } else { 246 statelessChild, err = block.BuildUnsigned( 247 parentID, 248 newTimestamp, 249 pChainHeight, 250 innerBlock.Bytes(), 251 ) 252 } 253 if err != nil { 254 p.vm.ctx.Log.Error("unexpected build block failure", 255 zap.String("reason", "failed to generate proposervm block header"), 256 zap.Stringer("parentID", parentID), 257 zap.Stringer("blkID", innerBlock.ID()), 258 zap.Error(err), 259 ) 260 return nil, err 261 } 262 263 child := &postForkBlock{ 264 SignedBlock: statelessChild, 265 postForkCommonComponents: postForkCommonComponents{ 266 vm: p.vm, 267 innerBlk: innerBlock, 268 }, 269 } 270 271 p.vm.ctx.Log.Info("built block", 272 zap.Stringer("blkID", child.ID()), 273 zap.Stringer("innerBlkID", innerBlock.ID()), 274 zap.Uint64("height", child.Height()), 275 zap.Uint64("pChainHeight", pChainHeight), 276 zap.Time("parentTimestamp", parentTimestamp), 277 zap.Time("blockTimestamp", newTimestamp), 278 ) 279 return child, nil 280 } 281 282 func (p *postForkCommonComponents) getInnerBlk() snowman.Block { 283 return p.innerBlk 284 } 285 286 func (p *postForkCommonComponents) setInnerBlk(innerBlk snowman.Block) { 287 p.innerBlk = innerBlk 288 } 289 290 func verifyIsOracleBlock(ctx context.Context, b snowman.Block) error { 291 oracle, ok := b.(snowman.OracleBlock) 292 if !ok { 293 return fmt.Errorf( 294 "%w: expected block %s to be a snowman.OracleBlock but it's a %T", 295 errUnexpectedBlockType, b.ID(), b, 296 ) 297 } 298 _, err := oracle.Options(ctx) 299 return err 300 } 301 302 func verifyIsNotOracleBlock(ctx context.Context, b snowman.Block) error { 303 oracle, ok := b.(snowman.OracleBlock) 304 if !ok { 305 return nil 306 } 307 _, err := oracle.Options(ctx) 308 switch err { 309 case nil: 310 return fmt.Errorf( 311 "%w: expected block %s not to be an oracle block but it's a %T", 312 errUnexpectedBlockType, b.ID(), b, 313 ) 314 case snowman.ErrNotOracle: 315 return nil 316 default: 317 return err 318 } 319 } 320 321 func (p *postForkCommonComponents) verifyPreDurangoBlockDelay( 322 ctx context.Context, 323 parentTimestamp time.Time, 324 parentPChainHeight uint64, 325 blk *postForkBlock, 326 ) (bool, error) { 327 var ( 328 blkTimestamp = blk.Timestamp() 329 childHeight = blk.Height() 330 proposerID = blk.Proposer() 331 ) 332 minDelay, err := p.vm.Windower.Delay( 333 ctx, 334 childHeight, 335 parentPChainHeight, 336 proposerID, 337 proposer.MaxVerifyWindows, 338 ) 339 if err != nil { 340 p.vm.ctx.Log.Error("unexpected block verification failure", 341 zap.String("reason", "failed to calculate required timestamp delay"), 342 zap.Stringer("blkID", blk.ID()), 343 zap.Error(err), 344 ) 345 return false, err 346 } 347 348 delay := blkTimestamp.Sub(parentTimestamp) 349 if delay < minDelay { 350 return false, fmt.Errorf("%w: delay %s < minDelay %s", errProposerWindowNotStarted, delay, minDelay) 351 } 352 353 return delay < proposer.MaxVerifyDelay, nil 354 } 355 356 func (p *postForkCommonComponents) verifyPostDurangoBlockDelay( 357 ctx context.Context, 358 parentTimestamp time.Time, 359 parentPChainHeight uint64, 360 blk *postForkBlock, 361 ) (bool, error) { 362 var ( 363 blkTimestamp = blk.Timestamp() 364 blkHeight = blk.Height() 365 currentSlot = proposer.TimeToSlot(parentTimestamp, blkTimestamp) 366 proposerID = blk.Proposer() 367 ) 368 // populate the slot for the block. 369 blk.slot = ¤tSlot 370 371 // find the expected proposer 372 expectedProposerID, err := p.vm.Windower.ExpectedProposer( 373 ctx, 374 blkHeight, 375 parentPChainHeight, 376 currentSlot, 377 ) 378 switch { 379 case errors.Is(err, proposer.ErrAnyoneCanPropose): 380 return false, nil // block should be unsigned 381 case err != nil: 382 p.vm.ctx.Log.Error("unexpected block verification failure", 383 zap.String("reason", "failed to calculate expected proposer"), 384 zap.Stringer("blkID", blk.ID()), 385 zap.Error(err), 386 ) 387 return false, err 388 case expectedProposerID == proposerID: 389 return true, nil // block should be signed 390 default: 391 return false, fmt.Errorf("%w: slot %d expects %s", errUnexpectedProposer, currentSlot, expectedProposerID) 392 } 393 } 394 395 func (p *postForkCommonComponents) shouldBuildSignedBlockPostDurango( 396 ctx context.Context, 397 parentID ids.ID, 398 parentTimestamp time.Time, 399 parentPChainHeight uint64, 400 newTimestamp time.Time, 401 ) (bool, error) { 402 parentHeight := p.innerBlk.Height() 403 currentSlot := proposer.TimeToSlot(parentTimestamp, newTimestamp) 404 expectedProposerID, err := p.vm.Windower.ExpectedProposer( 405 ctx, 406 parentHeight+1, 407 parentPChainHeight, 408 currentSlot, 409 ) 410 switch { 411 case errors.Is(err, proposer.ErrAnyoneCanPropose): 412 return false, nil // build an unsigned block 413 case err != nil: 414 p.vm.ctx.Log.Error("unexpected build block failure", 415 zap.String("reason", "failed to calculate expected proposer"), 416 zap.Stringer("parentID", parentID), 417 zap.Error(err), 418 ) 419 return false, err 420 case expectedProposerID == p.vm.ctx.NodeID: 421 return true, nil // build a signed block 422 } 423 424 // It's not our turn to propose a block yet. This is likely caused by having 425 // previously notified the consensus engine to attempt to build a block on 426 // top of a block that is no longer the preferred block. 427 p.vm.ctx.Log.Debug("build block dropped", 428 zap.Time("parentTimestamp", parentTimestamp), 429 zap.Time("blockTimestamp", newTimestamp), 430 zap.Uint64("slot", currentSlot), 431 zap.Stringer("expectedProposer", expectedProposerID), 432 ) 433 434 // We need to reschedule the block builder to the next time we can try to 435 // build a block. 436 // 437 // TODO: After Durango activates, restructure this logic to separate 438 // updating the scheduler from verifying the proposerID. 439 nextStartTime, err := p.vm.getPostDurangoSlotTime( 440 ctx, 441 parentHeight+1, 442 parentPChainHeight, 443 currentSlot+1, // We know we aren't the proposer for the current slot 444 parentTimestamp, 445 ) 446 if err != nil { 447 p.vm.ctx.Log.Error("failed to reset block builder scheduler", 448 zap.String("reason", "failed to calculate expected proposer"), 449 zap.Stringer("parentID", parentID), 450 zap.Error(err), 451 ) 452 return false, err 453 } 454 455 // report the build slot to the metrics. 456 p.vm.proposerBuildSlotGauge.Set(float64(proposer.TimeToSlot(parentTimestamp, nextStartTime))) 457 458 // set the scheduler to let us know when the next block need to be built. 459 p.vm.Scheduler.SetBuildBlockTime(nextStartTime) 460 461 // In case the inner VM only issued one pendingTxs message, we should 462 // attempt to re-handle that once it is our turn to build the block. 463 p.vm.notifyInnerBlockReady() 464 return false, fmt.Errorf("%w: slot %d expects %s", errUnexpectedProposer, currentSlot, expectedProposerID) 465 } 466 467 func (p *postForkCommonComponents) shouldBuildSignedBlockPreDurango( 468 ctx context.Context, 469 parentID ids.ID, 470 parentTimestamp time.Time, 471 parentPChainHeight uint64, 472 newTimestamp time.Time, 473 ) (bool, error) { 474 delay := newTimestamp.Sub(parentTimestamp) 475 if delay >= proposer.MaxBuildDelay { 476 return false, nil // time for any node to build an unsigned block 477 } 478 479 parentHeight := p.innerBlk.Height() 480 proposerID := p.vm.ctx.NodeID 481 minDelay, err := p.vm.Windower.Delay(ctx, parentHeight+1, parentPChainHeight, proposerID, proposer.MaxBuildWindows) 482 if err != nil { 483 p.vm.ctx.Log.Error("unexpected build block failure", 484 zap.String("reason", "failed to calculate required timestamp delay"), 485 zap.Stringer("parentID", parentID), 486 zap.Error(err), 487 ) 488 return false, err 489 } 490 491 if delay >= minDelay { 492 // it's time for this node to propose a block. It'll be signed or 493 // unsigned depending on the delay 494 return delay < proposer.MaxVerifyDelay, nil 495 } 496 497 // It's not our turn to propose a block yet. This is likely caused by having 498 // previously notified the consensus engine to attempt to build a block on 499 // top of a block that is no longer the preferred block. 500 p.vm.ctx.Log.Debug("build block dropped", 501 zap.Time("parentTimestamp", parentTimestamp), 502 zap.Duration("minDelay", minDelay), 503 zap.Time("blockTimestamp", newTimestamp), 504 ) 505 506 // In case the inner VM only issued one pendingTxs message, we should 507 // attempt to re-handle that once it is our turn to build the block. 508 p.vm.notifyInnerBlockReady() 509 return false, fmt.Errorf("%w: delay %s < minDelay %s", errProposerWindowNotStarted, delay, minDelay) 510 }