github.com/onflow/flow-go@v0.33.17/state/protocol/badger/snapshot.go (about) 1 // (c) 2019 Dapper Labs - ALL RIGHTS RESERVED 2 3 package badger 4 5 import ( 6 "errors" 7 "fmt" 8 9 "github.com/dgraph-io/badger/v2" 10 11 "github.com/onflow/flow-go/consensus/hotstuff/model" 12 "github.com/onflow/flow-go/model/flow" 13 "github.com/onflow/flow-go/model/flow/filter" 14 "github.com/onflow/flow-go/model/flow/mapfunc" 15 "github.com/onflow/flow-go/state/fork" 16 "github.com/onflow/flow-go/state/protocol" 17 "github.com/onflow/flow-go/state/protocol/inmem" 18 "github.com/onflow/flow-go/state/protocol/invalid" 19 "github.com/onflow/flow-go/storage" 20 "github.com/onflow/flow-go/storage/badger/operation" 21 "github.com/onflow/flow-go/storage/badger/procedure" 22 ) 23 24 // Snapshot implements the protocol.Snapshot interface. 25 // It represents a read-only immutable snapshot of the protocol state at the 26 // block it is constructed with. It allows efficient access to data associated directly 27 // with blocks at a given state (finalized, sealed), such as the related header, commit, 28 // seed or descending blocks. A block snapshot can lazily convert to an epoch snapshot in 29 // order to make data associated directly with epochs accessible through its API. 30 type Snapshot struct { 31 state *State 32 blockID flow.Identifier // reference block for this snapshot 33 } 34 35 // FinalizedSnapshot represents a read-only immutable snapshot of the protocol state 36 // at a finalized block. It is guaranteed to have a header available. 37 type FinalizedSnapshot struct { 38 Snapshot 39 header *flow.Header 40 } 41 42 var _ protocol.Snapshot = (*Snapshot)(nil) 43 var _ protocol.Snapshot = (*FinalizedSnapshot)(nil) 44 45 // newSnapshotWithIncorporatedReferenceBlock creates a new state snapshot with the given reference block. 46 // CAUTION: The caller is responsible for ensuring that the reference block has been incorporated. 47 func newSnapshotWithIncorporatedReferenceBlock(state *State, blockID flow.Identifier) *Snapshot { 48 return &Snapshot{ 49 state: state, 50 blockID: blockID, 51 } 52 } 53 54 // NewFinalizedSnapshot instantiates a `FinalizedSnapshot`. 55 // CAUTION: the header's ID _must_ match `blockID` (not checked) 56 func NewFinalizedSnapshot(state *State, blockID flow.Identifier, header *flow.Header) *FinalizedSnapshot { 57 return &FinalizedSnapshot{ 58 Snapshot: Snapshot{ 59 state: state, 60 blockID: blockID, 61 }, 62 header: header, 63 } 64 } 65 66 func (s *FinalizedSnapshot) Head() (*flow.Header, error) { 67 return s.header, nil 68 } 69 70 func (s *Snapshot) Head() (*flow.Header, error) { 71 head, err := s.state.headers.ByBlockID(s.blockID) 72 return head, err 73 } 74 75 // QuorumCertificate (QC) returns a valid quorum certificate pointing to the 76 // header at this snapshot. 77 // The sentinel error storage.ErrNotFound is returned if the QC is unknown. 78 func (s *Snapshot) QuorumCertificate() (*flow.QuorumCertificate, error) { 79 qc, err := s.state.qcs.ByBlockID(s.blockID) 80 if err != nil { 81 return nil, fmt.Errorf("could not retrieve quorum certificate for (%x): %w", s.blockID, err) 82 } 83 return qc, nil 84 } 85 86 func (s *Snapshot) Phase() (flow.EpochPhase, error) { 87 status, err := s.state.epoch.statuses.ByBlockID(s.blockID) 88 if err != nil { 89 return flow.EpochPhaseUndefined, fmt.Errorf("could not retrieve epoch status: %w", err) 90 } 91 phase, err := status.Phase() 92 return phase, err 93 } 94 95 func (s *Snapshot) Identities(selector flow.IdentityFilter) (flow.IdentityList, error) { 96 97 // TODO: CAUTION SHORTCUT 98 // we retrieve identities based on the initial identity table from the EpochSetup 99 // event here -- this will need revision to support mid-epoch identity changes 100 // once slashing is implemented 101 102 status, err := s.state.epoch.statuses.ByBlockID(s.blockID) 103 if err != nil { 104 return nil, err 105 } 106 107 setup, err := s.state.epoch.setups.ByID(status.CurrentEpoch.SetupID) 108 if err != nil { 109 return nil, err 110 } 111 112 // sort the identities so the 'IsCached' binary search works 113 identities := setup.Participants.Sort(flow.Canonical) 114 115 // get identities that are in either last/next epoch but NOT in the current epoch 116 var otherEpochIdentities flow.IdentityList 117 phase, err := status.Phase() 118 if err != nil { 119 return nil, fmt.Errorf("could not get phase: %w", err) 120 } 121 switch phase { 122 // during staking phase (the beginning of the epoch) we include identities 123 // from the previous epoch that are now un-staking 124 case flow.EpochPhaseStaking: 125 126 if !status.HasPrevious() { 127 break 128 } 129 130 previousSetup, err := s.state.epoch.setups.ByID(status.PreviousEpoch.SetupID) 131 if err != nil { 132 return nil, fmt.Errorf("could not get previous epoch setup event: %w", err) 133 } 134 135 for _, identity := range previousSetup.Participants { 136 exists := identities.Exists(identity) 137 // add identity from previous epoch that is not in current epoch 138 if !exists { 139 otherEpochIdentities = append(otherEpochIdentities, identity) 140 } 141 } 142 143 // during setup and committed phases (the end of the epoch) we include 144 // identities that will join in the next epoch 145 case flow.EpochPhaseSetup, flow.EpochPhaseCommitted: 146 147 nextSetup, err := s.state.epoch.setups.ByID(status.NextEpoch.SetupID) 148 if err != nil { 149 return nil, fmt.Errorf("could not get next epoch setup: %w", err) 150 } 151 152 for _, identity := range nextSetup.Participants { 153 exists := identities.Exists(identity) 154 155 // add identity from next epoch that is not in current epoch 156 if !exists { 157 otherEpochIdentities = append(otherEpochIdentities, identity) 158 } 159 } 160 161 default: 162 return nil, fmt.Errorf("invalid epoch phase: %s", phase) 163 } 164 165 // add the identities from next/last epoch, with weight set to 0 166 identities = append( 167 identities, 168 otherEpochIdentities.Map(mapfunc.WithWeight(0))..., 169 ) 170 171 // apply the filter to the participants 172 identities = identities.Filter(selector) 173 174 // apply a deterministic sort to the participants 175 identities = identities.Sort(flow.Canonical) 176 177 return identities, nil 178 } 179 180 func (s *Snapshot) Identity(nodeID flow.Identifier) (*flow.Identity, error) { 181 // filter identities at snapshot for node ID 182 identities, err := s.Identities(filter.HasNodeID(nodeID)) 183 if err != nil { 184 return nil, fmt.Errorf("could not get identities: %w", err) 185 } 186 187 // check if node ID is part of identities 188 if len(identities) == 0 { 189 return nil, protocol.IdentityNotFoundError{NodeID: nodeID} 190 } 191 return identities[0], nil 192 } 193 194 // Commit retrieves the latest execution state commitment at the current block snapshot. This 195 // commitment represents the execution state as currently finalized. 196 func (s *Snapshot) Commit() (flow.StateCommitment, error) { 197 // get the ID of the sealed block 198 seal, err := s.state.seals.HighestInFork(s.blockID) 199 if err != nil { 200 return flow.DummyStateCommitment, fmt.Errorf("could not retrieve sealed state commit: %w", err) 201 } 202 return seal.FinalState, nil 203 } 204 205 func (s *Snapshot) SealedResult() (*flow.ExecutionResult, *flow.Seal, error) { 206 seal, err := s.state.seals.HighestInFork(s.blockID) 207 if err != nil { 208 return nil, nil, fmt.Errorf("could not look up latest seal: %w", err) 209 } 210 result, err := s.state.results.ByID(seal.ResultID) 211 if err != nil { 212 return nil, nil, fmt.Errorf("could not get latest result: %w", err) 213 } 214 return result, seal, nil 215 } 216 217 // SealingSegment will walk through the chain backward until we reach the block referenced 218 // by the latest seal and build a SealingSegment. As we visit each block we check each execution 219 // receipt in the block's payload to make sure we have a corresponding execution result, any 220 // execution results missing from blocks are stored in the `SealingSegment.ExecutionResults` field. 221 // See `model/flow/sealing_segment.md` for detailed technical specification of the Sealing Segment 222 // 223 // Expected errors during normal operations: 224 // - protocol.ErrSealingSegmentBelowRootBlock if sealing segment would stretch beyond the node's local history cut-off 225 // - protocol.UnfinalizedSealingSegmentError if sealing segment would contain unfinalized blocks (including orphaned blocks) 226 func (s *Snapshot) SealingSegment() (*flow.SealingSegment, error) { 227 // Lets denote the highest block in the sealing segment `head` (initialized below). 228 // Based on the tech spec `flow/sealing_segment.md`, the Sealing Segment must contain contain 229 // enough history to satisfy _all_ of the following conditions: 230 // (i) The highest sealed block as of `head` needs to be included in the sealing segment. 231 // This is relevant if `head` does not contain any seals. 232 // (ii) All blocks that are sealed by `head`. This is relevant if head` contains _multiple_ seals. 233 // (iii) The sealing segment should contain the history back to (including): 234 // limitHeight := max(blockSealedAtHead.Height - flow.ExtraBlocksInRootSealingSegment, SporkRootBlockHeight) 235 // Per convention, we include the blocks for (i) in the `SealingSegment.Blocks`, while the 236 // additional blocks for (ii) and optionally (iii) are contained in as `SealingSegment.ExtraBlocks`. 237 head, err := s.state.blocks.ByID(s.blockID) 238 if err != nil { 239 return nil, fmt.Errorf("could not get snapshot's reference block: %w", err) 240 } 241 if head.Header.Height < s.state.finalizedRootHeight { 242 return nil, protocol.ErrSealingSegmentBelowRootBlock 243 } 244 245 // Verify that head of sealing segment is finalized. 246 finalizedBlockAtHeight, err := s.state.headers.BlockIDByHeight(head.Header.Height) 247 if err != nil { 248 if errors.Is(err, storage.ErrNotFound) { 249 return nil, protocol.NewUnfinalizedSealingSegmentErrorf("head of sealing segment at height %d is not finalized: %w", head.Header.Height, err) 250 } 251 return nil, fmt.Errorf("exception while retrieving finzalized bloc, by height: %w", err) 252 } 253 if finalizedBlockAtHeight != s.blockID { // comparison of fixed-length arrays 254 return nil, protocol.NewUnfinalizedSealingSegmentErrorf("head of sealing segment is orphaned, finalized block at height %d is %x", head.Header.Height, finalizedBlockAtHeight) 255 } 256 257 // STEP (i): highest sealed block as of `head` must be included. 258 seal, err := s.state.seals.HighestInFork(s.blockID) 259 if err != nil { 260 return nil, fmt.Errorf("could not get seal for sealing segment: %w", err) 261 } 262 blockSealedAtHead, err := s.state.headers.ByBlockID(seal.BlockID) 263 if err != nil { 264 return nil, fmt.Errorf("could not get block: %w", err) 265 } 266 267 // walk through the chain backward until we reach the block referenced by 268 // the latest seal - the returned segment includes this block 269 builder := flow.NewSealingSegmentBuilder(s.state.results.ByID, s.state.seals.HighestInFork) 270 scraper := func(header *flow.Header) error { 271 blockID := header.ID() 272 block, err := s.state.blocks.ByID(blockID) 273 if err != nil { 274 return fmt.Errorf("could not get block: %w", err) 275 } 276 277 err = builder.AddBlock(block) 278 if err != nil { 279 return fmt.Errorf("could not add block to sealing segment: %w", err) 280 } 281 282 return nil 283 } 284 err = fork.TraverseForward(s.state.headers, s.blockID, scraper, fork.IncludingBlock(seal.BlockID)) 285 if err != nil { 286 return nil, fmt.Errorf("could not traverse sealing segment: %w", err) 287 } 288 289 // STEP (ii): extend history down to the lowest block, whose seal is included in `head` 290 lowestSealedByHead := blockSealedAtHead 291 for _, sealInHead := range head.Payload.Seals { 292 h, e := s.state.headers.ByBlockID(sealInHead.BlockID) 293 if e != nil { 294 return nil, fmt.Errorf("could not get block (id=%x) for seal: %w", seal.BlockID, e) // storage.ErrNotFound or exception 295 } 296 if h.Height < lowestSealedByHead.Height { 297 lowestSealedByHead = h 298 } 299 } 300 301 // STEP (iii): extended history to allow checking for duplicated collections, i.e. 302 // limitHeight = max(blockSealedAtHead.Height - flow.ExtraBlocksInRootSealingSegment, SporkRootBlockHeight) 303 limitHeight := s.state.sporkRootBlockHeight 304 if blockSealedAtHead.Height > s.state.sporkRootBlockHeight+flow.ExtraBlocksInRootSealingSegment { 305 limitHeight = blockSealedAtHead.Height - flow.ExtraBlocksInRootSealingSegment 306 } 307 308 // As we have to satisfy (ii) _and_ (iii), we have to take the longest history, i.e. the lowest height. 309 if lowestSealedByHead.Height < limitHeight { 310 limitHeight = lowestSealedByHead.Height 311 if limitHeight < s.state.sporkRootBlockHeight { // sanity check; should never happen 312 return nil, fmt.Errorf("unexpected internal error: calculated history-cutoff at height %d, which is lower than the spork's root height %d", limitHeight, s.state.sporkRootBlockHeight) 313 } 314 } 315 if limitHeight < blockSealedAtHead.Height { 316 // we need to include extra blocks in sealing segment 317 extraBlocksScraper := func(header *flow.Header) error { 318 blockID := header.ID() 319 block, err := s.state.blocks.ByID(blockID) 320 if err != nil { 321 return fmt.Errorf("could not get block: %w", err) 322 } 323 324 err = builder.AddExtraBlock(block) 325 if err != nil { 326 return fmt.Errorf("could not add block to sealing segment: %w", err) 327 } 328 329 return nil 330 } 331 332 err = fork.TraverseBackward(s.state.headers, blockSealedAtHead.ParentID, extraBlocksScraper, fork.IncludingHeight(limitHeight)) 333 if err != nil { 334 return nil, fmt.Errorf("could not traverse extra blocks for sealing segment: %w", err) 335 } 336 } 337 338 segment, err := builder.SealingSegment() 339 if err != nil { 340 return nil, fmt.Errorf("could not build sealing segment: %w", err) 341 } 342 343 return segment, nil 344 } 345 346 func (s *Snapshot) Descendants() ([]flow.Identifier, error) { 347 descendants, err := s.descendants(s.blockID) 348 if err != nil { 349 return nil, fmt.Errorf("failed to traverse the descendants tree of block %v: %w", s.blockID, err) 350 } 351 return descendants, nil 352 } 353 354 func (s *Snapshot) lookupChildren(blockID flow.Identifier) ([]flow.Identifier, error) { 355 var children flow.IdentifierList 356 err := s.state.db.View(procedure.LookupBlockChildren(blockID, &children)) 357 if err != nil { 358 return nil, fmt.Errorf("could not get children of block %v: %w", blockID, err) 359 } 360 return children, nil 361 } 362 363 func (s *Snapshot) descendants(blockID flow.Identifier) ([]flow.Identifier, error) { 364 descendantIDs, err := s.lookupChildren(blockID) 365 if err != nil { 366 return nil, err 367 } 368 369 for _, descendantID := range descendantIDs { 370 additionalIDs, err := s.descendants(descendantID) 371 if err != nil { 372 return nil, err 373 } 374 descendantIDs = append(descendantIDs, additionalIDs...) 375 } 376 return descendantIDs, nil 377 } 378 379 // RandomSource returns the seed for the current block's snapshot. 380 // Expected error returns: 381 // * storage.ErrNotFound is returned if the QC is unknown. 382 func (s *Snapshot) RandomSource() ([]byte, error) { 383 qc, err := s.QuorumCertificate() 384 if err != nil { 385 return nil, err 386 } 387 randomSource, err := model.BeaconSignature(qc) 388 if err != nil { 389 return nil, fmt.Errorf("could not create seed from QC's signature: %w", err) 390 } 391 return randomSource, nil 392 } 393 394 func (s *Snapshot) Epochs() protocol.EpochQuery { 395 return &EpochQuery{ 396 snap: s, 397 } 398 } 399 400 func (s *Snapshot) Params() protocol.GlobalParams { 401 return s.state.Params() 402 } 403 404 func (s *Snapshot) VersionBeacon() (*flow.SealedVersionBeacon, error) { 405 head, err := s.state.headers.ByBlockID(s.blockID) 406 if err != nil { 407 return nil, err 408 } 409 410 return s.state.versionBeacons.Highest(head.Height) 411 } 412 413 // EpochQuery encapsulates querying epochs w.r.t. a snapshot. 414 type EpochQuery struct { 415 snap *Snapshot 416 } 417 418 // Current returns the current epoch. 419 func (q *EpochQuery) Current() protocol.Epoch { 420 // all errors returned from storage reads here are unexpected, because all 421 // snapshots reside within a current epoch, which must be queryable 422 status, err := q.snap.state.epoch.statuses.ByBlockID(q.snap.blockID) 423 if err != nil { 424 return invalid.NewEpochf("could not get epoch status for block %x: %w", q.snap.blockID, err) 425 } 426 setup, err := q.snap.state.epoch.setups.ByID(status.CurrentEpoch.SetupID) 427 if err != nil { 428 return invalid.NewEpochf("could not get current EpochSetup (id=%x) for block %x: %w", status.CurrentEpoch.SetupID, q.snap.blockID, err) 429 } 430 commit, err := q.snap.state.epoch.commits.ByID(status.CurrentEpoch.CommitID) 431 if err != nil { 432 return invalid.NewEpochf("could not get current EpochCommit (id=%x) for block %x: %w", status.CurrentEpoch.CommitID, q.snap.blockID, err) 433 } 434 435 firstHeight, _, epochStarted, _, err := q.retrieveEpochHeightBounds(setup.Counter) 436 if err != nil { 437 return invalid.NewEpochf("could not get current epoch height bounds: %s", err.Error()) 438 } 439 if epochStarted { 440 return inmem.NewStartedEpoch(setup, commit, firstHeight) 441 } 442 return inmem.NewCommittedEpoch(setup, commit) 443 } 444 445 // Next returns the next epoch, if it is available. 446 func (q *EpochQuery) Next() protocol.Epoch { 447 448 status, err := q.snap.state.epoch.statuses.ByBlockID(q.snap.blockID) 449 if err != nil { 450 return invalid.NewEpochf("could not get epoch status for block %x: %w", q.snap.blockID, err) 451 } 452 phase, err := status.Phase() 453 if err != nil { 454 // critical error: malformed EpochStatus in storage 455 return invalid.NewEpochf("read malformed EpochStatus from storage: %w", err) 456 } 457 // if we are in the staking phase, the next epoch is not setup yet 458 if phase == flow.EpochPhaseStaking { 459 return invalid.NewEpoch(protocol.ErrNextEpochNotSetup) 460 } 461 462 // if we are in setup phase, return a SetupEpoch 463 nextSetup, err := q.snap.state.epoch.setups.ByID(status.NextEpoch.SetupID) 464 if err != nil { 465 // all errors are critical, because we must be able to retrieve EpochSetup when in setup phase 466 return invalid.NewEpochf("could not get next EpochSetup (id=%x) for block %x: %w", status.NextEpoch.SetupID, q.snap.blockID, err) 467 } 468 if phase == flow.EpochPhaseSetup { 469 return inmem.NewSetupEpoch(nextSetup) 470 } 471 472 // if we are in committed phase, return a CommittedEpoch 473 nextCommit, err := q.snap.state.epoch.commits.ByID(status.NextEpoch.CommitID) 474 if err != nil { 475 // all errors are critical, because we must be able to retrieve EpochCommit when in committed phase 476 return invalid.NewEpochf("could not get next EpochCommit (id=%x) for block %x: %w", status.NextEpoch.CommitID, q.snap.blockID, err) 477 } 478 return inmem.NewCommittedEpoch(nextSetup, nextCommit) 479 } 480 481 // Previous returns the previous epoch. During the first epoch after the root 482 // block, this returns a sentinel error (since there is no previous epoch). 483 // For all other epochs, returns the previous epoch. 484 func (q *EpochQuery) Previous() protocol.Epoch { 485 486 status, err := q.snap.state.epoch.statuses.ByBlockID(q.snap.blockID) 487 if err != nil { 488 return invalid.NewEpochf("could not get epoch status for block %x: %w", q.snap.blockID, err) 489 } 490 491 // CASE 1: there is no previous epoch - this indicates we are in the first 492 // epoch after a spork root or genesis block 493 if !status.HasPrevious() { 494 return invalid.NewEpoch(protocol.ErrNoPreviousEpoch) 495 } 496 497 // CASE 2: we are in any other epoch - retrieve the setup and commit events 498 // for the previous epoch 499 setup, err := q.snap.state.epoch.setups.ByID(status.PreviousEpoch.SetupID) 500 if err != nil { 501 // all errors are critical, because we must be able to retrieve EpochSetup for previous epoch 502 return invalid.NewEpochf("could not get previous EpochSetup (id=%x) for block %x: %w", status.PreviousEpoch.SetupID, q.snap.blockID, err) 503 } 504 commit, err := q.snap.state.epoch.commits.ByID(status.PreviousEpoch.CommitID) 505 if err != nil { 506 // all errors are critical, because we must be able to retrieve EpochCommit for previous epoch 507 return invalid.NewEpochf("could not get current EpochCommit (id=%x) for block %x: %w", status.PreviousEpoch.CommitID, q.snap.blockID, err) 508 } 509 510 firstHeight, finalHeight, _, epochEnded, err := q.retrieveEpochHeightBounds(setup.Counter) 511 if err != nil { 512 return invalid.NewEpochf("could not get epoch height bounds: %w", err) 513 } 514 if epochEnded { 515 return inmem.NewEndedEpoch(setup, commit, firstHeight, finalHeight) 516 } 517 return inmem.NewStartedEpoch(setup, commit, firstHeight) 518 } 519 520 // retrieveEpochHeightBounds retrieves the height bounds for an epoch. 521 // Height bounds are NOT fork-aware, and are only determined upon finalization. 522 // 523 // Since the protocol state's API is fork-aware, we may be querying an 524 // un-finalized block, as in the following example: 525 // 526 // Epoch 1 Epoch 2 527 // A <- B <-|- C <- D 528 // 529 // Suppose block B is the latest finalized block and we have queried block D. 530 // Then, the transition from epoch 1 to 2 has not been committed, because the first block of epoch 2 has not been finalized. 531 // In this case, the final block of Epoch 1, from the perspective of block D, is unknown. 532 // There are edge-case scenarios, where a different fork could exist (as illustrated below) 533 // that still adds additional blocks to Epoch 1. 534 // 535 // Epoch 1 Epoch 2 536 // A <- B <---|-- C <- D 537 // ^ 538 // ╰ X <-|- X <- Y <- Z 539 // 540 // Returns: 541 // - (0, 0, false, false, nil) if epoch is not started 542 // - (firstHeight, 0, true, false, nil) if epoch is started but not ended 543 // - (firstHeight, finalHeight, true, true, nil) if epoch is ended 544 // 545 // No errors are expected during normal operation. 546 func (q *EpochQuery) retrieveEpochHeightBounds(epoch uint64) (firstHeight, finalHeight uint64, isFirstBlockFinalized, isLastBlockFinalized bool, err error) { 547 err = q.snap.state.db.View(func(tx *badger.Txn) error { 548 // Retrieve the epoch's first height 549 err = operation.RetrieveEpochFirstHeight(epoch, &firstHeight)(tx) 550 if err != nil { 551 if errors.Is(err, storage.ErrNotFound) { 552 isFirstBlockFinalized = false 553 isLastBlockFinalized = false 554 return nil 555 } 556 return err // unexpected error 557 } 558 isFirstBlockFinalized = true 559 560 var subsequentEpochFirstHeight uint64 561 err = operation.RetrieveEpochFirstHeight(epoch+1, &subsequentEpochFirstHeight)(tx) 562 if err != nil { 563 if errors.Is(err, storage.ErrNotFound) { 564 isLastBlockFinalized = false 565 return nil 566 } 567 return err // unexpected error 568 } 569 finalHeight = subsequentEpochFirstHeight - 1 570 isLastBlockFinalized = true 571 572 return nil 573 }) 574 if err != nil { 575 return 0, 0, false, false, err 576 } 577 return firstHeight, finalHeight, isFirstBlockFinalized, isLastBlockFinalized, nil 578 }