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