diff --git a/beacon-chain/db/iface/interface.go b/beacon-chain/db/iface/interface.go index 7b507c9c87..fa11c76786 100644 --- a/beacon-chain/db/iface/interface.go +++ b/beacon-chain/db/iface/interface.go @@ -30,7 +30,7 @@ type ReadOnlyDatabase interface { GenesisBlockRoot(ctx context.Context) ([32]byte, error) IsFinalizedBlock(ctx context.Context, blockRoot [32]byte) bool FinalizedChildBlock(ctx context.Context, blockRoot [32]byte) (interfaces.SignedBeaconBlock, error) - HighestBlockBelowSlot(ctx context.Context, slot types.Slot) (interfaces.SignedBeaconBlock, error) + HighestSlotBlocksBelow(ctx context.Context, slot types.Slot) ([]interfaces.SignedBeaconBlock, error) // State related methods. State(ctx context.Context, blockRoot [32]byte) (state.BeaconState, error) StateOrError(ctx context.Context, blockRoot [32]byte) (state.BeaconState, error) diff --git a/beacon-chain/db/kv/blocks.go b/beacon-chain/db/kv/blocks.go index bfdd632ec8..07d8bdb9e7 100644 --- a/beacon-chain/db/kv/blocks.go +++ b/beacon-chain/db/kv/blocks.go @@ -398,9 +398,9 @@ func (s *Store) SaveBackfillBlockRoot(ctx context.Context, blockRoot [32]byte) e }) } -// HighestBlockBelowSlot returns the block with the highest slot that's less than the slot argument. -func (s *Store) HighestBlockBelowSlot(ctx context.Context, slot types.Slot) (interfaces.SignedBeaconBlock, error) { - ctx, span := trace.StartSpan(ctx, "BeaconDB.HighestBlockBelowSlot") +// HighestSlotBlocksBelow returns the block with the highest slot below the input slot from the db. +func (s *Store) HighestSlotBlocksBelow(ctx context.Context, slot types.Slot) ([]interfaces.SignedBeaconBlock, error) { + ctx, span := trace.StartSpan(ctx, "BeaconDB.HighestSlotBlocksBelow") defer span.End() var root [32]byte @@ -457,7 +457,7 @@ func (s *Store) HighestBlockBelowSlot(ctx context.Context, slot types.Slot) (int return nil, err } } - return blk, nil + return []interfaces.SignedBeaconBlock{blk}, nil } // FeeRecipientByValidatorID returns the fee recipient for a validator id. diff --git a/beacon-chain/db/kv/blocks_test.go b/beacon-chain/db/kv/blocks_test.go index d10c205fb4..160fc3bb09 100644 --- a/beacon-chain/db/kv/blocks_test.go +++ b/beacon-chain/db/kv/blocks_test.go @@ -517,18 +517,18 @@ func TestStore_SaveBlock_CanGetHighestAt(t *testing.T) { require.NoError(t, db.SaveBlock(ctx, block2)) require.NoError(t, db.SaveBlock(ctx, block3)) - highestAt, err := db.HighestBlockBelowSlot(ctx, 2) + highestAt, err := db.HighestSlotBlocksBelow(ctx, 2) require.NoError(t, err) - assert.NotNil(t, highestAt, "Got empty highest block") - assert.Equal(t, true, proto.Equal(block1.Proto(), highestAt.Proto()), "Wanted: %v, received: %v", block1, highestAt) - highestAt, err = db.HighestBlockBelowSlot(ctx, 11) + assert.Equal(t, false, len(highestAt) <= 0, "Got empty highest at slice") + assert.Equal(t, true, proto.Equal(block1.Proto(), highestAt[0].Proto()), "Wanted: %v, received: %v", block1, highestAt[0]) + highestAt, err = db.HighestSlotBlocksBelow(ctx, 11) require.NoError(t, err) - assert.NotNil(t, highestAt, "Got empty highest block") - assert.Equal(t, true, proto.Equal(block2.Proto(), highestAt.Proto()), "Wanted: %v, received: %v", block2, highestAt) - highestAt, err = db.HighestBlockBelowSlot(ctx, 101) + assert.Equal(t, false, len(highestAt) <= 0, "Got empty highest at slice") + assert.Equal(t, true, proto.Equal(block2.Proto(), highestAt[0].Proto()), "Wanted: %v, received: %v", block2, highestAt[0]) + highestAt, err = db.HighestSlotBlocksBelow(ctx, 101) require.NoError(t, err) - assert.NotNil(t, highestAt, "Got empty highest block") - assert.Equal(t, true, proto.Equal(block3.Proto(), highestAt.Proto()), "Wanted: %v, received: %v", block3, highestAt) + assert.Equal(t, false, len(highestAt) <= 0, "Got empty highest at slice") + assert.Equal(t, true, proto.Equal(block3.Proto(), highestAt[0].Proto()), "Wanted: %v, received: %v", block3, highestAt[0]) }) } } @@ -549,15 +549,15 @@ func TestStore_GenesisBlock_CanGetHighestAt(t *testing.T) { require.NoError(t, err) require.NoError(t, db.SaveBlock(ctx, block1)) - highestAt, err := db.HighestBlockBelowSlot(ctx, 2) + highestAt, err := db.HighestSlotBlocksBelow(ctx, 2) require.NoError(t, err) - assert.Equal(t, true, proto.Equal(block1.Proto(), highestAt.Proto()), "Wanted: %v, received: %v", block1, highestAt) - highestAt, err = db.HighestBlockBelowSlot(ctx, 1) + assert.Equal(t, true, proto.Equal(block1.Proto(), highestAt[0].Proto()), "Wanted: %v, received: %v", block1, highestAt[0]) + highestAt, err = db.HighestSlotBlocksBelow(ctx, 1) require.NoError(t, err) - assert.Equal(t, true, proto.Equal(genesisBlock.Proto(), highestAt.Proto()), "Wanted: %v, received: %v", genesisBlock, highestAt) - highestAt, err = db.HighestBlockBelowSlot(ctx, 0) + assert.Equal(t, true, proto.Equal(genesisBlock.Proto(), highestAt[0].Proto()), "Wanted: %v, received: %v", genesisBlock, highestAt[0]) + highestAt, err = db.HighestSlotBlocksBelow(ctx, 0) require.NoError(t, err) - assert.Equal(t, true, proto.Equal(genesisBlock.Proto(), highestAt.Proto()), "Wanted: %v, received: %v", genesisBlock, highestAt) + assert.Equal(t, true, proto.Equal(genesisBlock.Proto(), highestAt[0].Proto()), "Wanted: %v, received: %v", genesisBlock, highestAt[0]) }) } } diff --git a/beacon-chain/state/stategen/history.go b/beacon-chain/state/stategen/history.go index 9bf1bc2f58..1af93a6936 100644 --- a/beacon-chain/state/stategen/history.go +++ b/beacon-chain/state/stategen/history.go @@ -55,11 +55,14 @@ func (c *CanonicalHistory) BlockForSlot(ctx context.Context, target types.Slot) if ctx.Err() != nil { return [32]byte{}, nil, errors.Wrap(ctx.Err(), "context canceled during canonicalBlockForSlot") } - b, err := c.h.HighestBlockBelowSlot(ctx, target+1) + hbs, err := c.h.HighestSlotBlocksBelow(ctx, target+1) if err != nil { return [32]byte{}, nil, errors.Wrap(err, fmt.Sprintf("error finding highest block w/ slot <= %d", target)) } - r, err := c.canonicalRoot(ctx, b) + if len(hbs) == 0 { + return [32]byte{}, nil, errors.Wrap(ErrNoBlocksBelowSlot, fmt.Sprintf("slot=%d", target)) + } + r, b, err := c.bestForSlot(ctx, hbs) if err == nil { // we found a valid, canonical block! return r, b, nil @@ -68,10 +71,10 @@ func (c *CanonicalHistory) BlockForSlot(ctx context.Context, target types.Slot) // we found a block, but it wasn't considered canonical - keep looking if errors.Is(err, ErrNoCanonicalBlockForSlot) { // break once we've seen slot 0 (and prevent underflow) - if b.Block().Slot() == params.BeaconConfig().GenesisSlot { + if hbs[0].Block().Slot() == params.BeaconConfig().GenesisSlot { break } - target = b.Block().Slot() - 1 + target = hbs[0].Block().Slot() - 1 continue } return [32]byte{}, nil, err @@ -80,34 +83,36 @@ func (c *CanonicalHistory) BlockForSlot(ctx context.Context, target types.Slot) if err != nil { return [32]byte{}, nil, errors.Wrap(err, "db error while retrieving genesis block") } - root, err := c.canonicalRoot(ctx, b) + root, _, err := c.bestForSlot(ctx, []interfaces.SignedBeaconBlock{b}) if err != nil { return [32]byte{}, nil, errors.Wrap(err, "problem retrieving genesis block") } return root, b, nil } -// canonicalRoot encapsulates several messy realities of the underlying db code, -// performing nil/validity checks, and using CanonicalChecker to ensure the block is canonical. -func (c *CanonicalHistory) canonicalRoot(ctx context.Context, b interfaces.SignedBeaconBlock) ([32]byte, error) { - if wrapper.BeaconBlockIsNil(b) != nil { - return [32]byte{}, errors.Wrap(ErrNoCanonicalBlockForSlot, "nil block") +// bestForSlot encapsulates several messy realities of the underlying db code, looping through multiple blocks, +// performing null/validity checks, and using CanonicalChecker to only pick canonical blocks. +func (c *CanonicalHistory) bestForSlot(ctx context.Context, hbs []interfaces.SignedBeaconBlock) ([32]byte, interfaces.SignedBeaconBlock, error) { + for _, b := range hbs { + if wrapper.BeaconBlockIsNil(b) != nil { + continue + } + root, err := b.Block().HashTreeRoot() + if err != nil { + // use this error message to wrap a sentinel error for error type matching + wrapped := errors.Wrap(ErrInvalidDBBlock, err.Error()) + msg := fmt.Sprintf("could not compute hash_tree_root for block at slot=%d", b.Block().Slot()) + return [32]byte{}, nil, errors.Wrap(wrapped, msg) + } + canon, err := c.cc.IsCanonical(ctx, root) + if err != nil { + return [32]byte{}, nil, errors.Wrap(err, "replayer could not check if block is canonical") + } + if canon { + return root, b, nil + } } - root, err := b.Block().HashTreeRoot() - if err != nil { - // use this error message to wrap a sentinel error for error type matching - wrapped := errors.Wrap(ErrInvalidDBBlock, err.Error()) - msg := fmt.Sprintf("could not compute hash_tree_root for block at slot=%d", b.Block().Slot()) - return [32]byte{}, errors.Wrap(wrapped, msg) - } - canon, err := c.cc.IsCanonical(ctx, root) - if err != nil { - return [32]byte{}, errors.Wrap(err, "replayer could not check if block is canonical") - } - if canon { - return root, nil - } - return [32]byte{}, errors.Wrap(ErrNoCanonicalBlockForSlot, "no good block for slot") + return [32]byte{}, nil, errors.Wrap(ErrNoCanonicalBlockForSlot, "no good block for slot") } // ChainForSlot creates a value that satisfies the Replayer interface via db queries diff --git a/beacon-chain/state/stategen/history_test.go b/beacon-chain/state/stategen/history_test.go index a7b2e63b34..4c7314887c 100644 --- a/beacon-chain/state/stategen/history_test.go +++ b/beacon-chain/state/stategen/history_test.go @@ -33,7 +33,7 @@ func TestChainForSlotFuture(t *testing.T) { require.ErrorIs(t, err, ErrFutureSlotRequested) } -func TestCanonicalRoot(t *testing.T) { +func TestBestForSlot(t *testing.T) { nilBlock, err := wrapper.WrappedSignedBeaconBlock(ðpb.SignedBeaconBlock{}) require.NoError(t, err) nilBody, err := wrapper.WrappedSignedBeaconBlock(ðpb.SignedBeaconBlock{Block: ðpb.BeaconBlock{}}) @@ -48,61 +48,70 @@ func TestCanonicalRoot(t *testing.T) { better := &mock.SignedBeaconBlock{BeaconBlock: &mock.BeaconBlock{BeaconBlockBody: &mock.BeaconBlockBody{}, Htr: betterHTR}} cases := []struct { - name string - err error - block interfaces.SignedBeaconBlock - root [32]byte - cc CanonicalChecker + name string + err error + blocks []interfaces.SignedBeaconBlock + best interfaces.SignedBeaconBlock + root [32]byte + cc CanonicalChecker }{ { - name: "empty SignedBeaconBlock", - err: ErrNoCanonicalBlockForSlot, - block: nil, + name: "empty list", + err: ErrNoCanonicalBlockForSlot, + blocks: []interfaces.SignedBeaconBlock{}, }, { - name: "empty BeaconBlock", - err: ErrNoCanonicalBlockForSlot, - block: nilBlock, + name: "empty SignedBeaconBlock", + err: ErrNoCanonicalBlockForSlot, + blocks: []interfaces.SignedBeaconBlock{nil}, }, { - name: "empty BeaconBlockBody", - err: ErrNoCanonicalBlockForSlot, - block: nilBody, + name: "empty BeaconBlock", + err: ErrNoCanonicalBlockForSlot, + blocks: []interfaces.SignedBeaconBlock{nilBlock}, }, { - name: "bad HTR", - err: ErrInvalidDBBlock, - block: badHTR, + name: "empty BeaconBlockBody", + err: ErrNoCanonicalBlockForSlot, + blocks: []interfaces.SignedBeaconBlock{nilBody}, }, { - name: "IsCanonical fail", - block: good, - cc: &mockCanonicalChecker{is: true, err: derp}, - err: derp, + name: "bad HTR", + err: ErrInvalidDBBlock, + blocks: []interfaces.SignedBeaconBlock{badHTR}, }, { - name: "all non-canonical", - err: ErrNoCanonicalBlockForSlot, - block: good, - cc: &mockCanonicalChecker{is: false}, + name: "IsCanonical fail", + blocks: []interfaces.SignedBeaconBlock{good, better}, + cc: &mockCanonicalChecker{is: true, err: derp}, + err: derp, }, { - name: "one canonical", - block: good, - cc: &mockCanonicalChecker{is: true}, - root: goodHTR, + name: "all non-canonical", + err: ErrNoCanonicalBlockForSlot, + blocks: []interfaces.SignedBeaconBlock{good, better}, + cc: &mockCanonicalChecker{is: false}, }, { - name: "all canonical", - block: better, - cc: &mockCanonicalChecker{is: true}, - root: betterHTR, + name: "one canonical", + blocks: []interfaces.SignedBeaconBlock{good}, + cc: &mockCanonicalChecker{is: true}, + root: goodHTR, + best: good, }, { - name: "first wins", - block: good, - cc: &mockCanonicalChecker{is: true}, - root: goodHTR, + name: "all canonical", + blocks: []interfaces.SignedBeaconBlock{better, good}, + cc: &mockCanonicalChecker{is: true}, + root: betterHTR, + best: better, + }, + { + name: "first wins", + blocks: []interfaces.SignedBeaconBlock{good, better}, + cc: &mockCanonicalChecker{is: true}, + root: goodHTR, + best: good, }, } for _, c := range cases { @@ -112,9 +121,10 @@ func TestCanonicalRoot(t *testing.T) { chk = c.cc } ch := &CanonicalHistory{cc: chk} - r, err := ch.canonicalRoot(context.Background(), c.block) + r, b, err := ch.bestForSlot(context.Background(), c.blocks) if c.err == nil { require.NoError(t, err) + require.DeepEqual(t, c.best, b) require.Equal(t, c.root, r) } else { require.ErrorIs(t, err, c.err) @@ -154,9 +164,10 @@ func TestCanonicalBlockForSlotHappy(t *testing.T) { } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - b, err := hist.HighestBlockBelowSlot(ctx, c.slot+1) + bs, err := hist.HighestSlotBlocksBelow(ctx, c.slot+1) require.NoError(t, err) - r, err := b.Block().HashTreeRoot() + require.Equal(t, len(bs), 1) + r, err := bs[0].Block().HashTreeRoot() require.NoError(t, err) require.Equal(t, hist.slotMap[c.highest], r) cr, _, err := ch.BlockForSlot(ctx, c.slot) @@ -177,35 +188,43 @@ func TestCanonicalBlockForSlotNonHappy(t *testing.T) { hist := newMockHistory(t, specs, end+1) slotOrderObserved := make([]types.Slot, 0) - derp := errors.New("HighestBlockBelowSlot don't work") + derp := errors.New("HighestSlotBlocksBelow don't work") // since only the end block and genesis are canonical, once the slot drops below // end, we should always get genesis cases := []struct { name string slot types.Slot canon CanonicalChecker - overrideHighest func(context.Context, types.Slot) (interfaces.SignedBeaconBlock, error) + overrideHighest func(context.Context, types.Slot) ([]interfaces.SignedBeaconBlock, error) slotOrderExpected []types.Slot err error root [32]byte }{ { - name: "HighestBlockBelowSlot not called for genesis", - overrideHighest: func(_ context.Context, _ types.Slot) (interfaces.SignedBeaconBlock, error) { + name: "HighestSlotBlocksBelow not called for genesis", + overrideHighest: func(_ context.Context, _ types.Slot) ([]interfaces.SignedBeaconBlock, error) { return nil, derp }, root: hist.slotMap[0], }, { - name: "wrapped error from HighestBlockBelowSlot returned", + name: "wrapped error from HighestSlotBlocksBelow returned", err: derp, - overrideHighest: func(_ context.Context, _ types.Slot) (interfaces.SignedBeaconBlock, error) { + overrideHighest: func(_ context.Context, _ types.Slot) ([]interfaces.SignedBeaconBlock, error) { return nil, derp }, slot: end, }, { - name: "HighestBlockBelowSlot no canonical", + name: "HighestSlotBlocksBelow empty list", + err: ErrNoBlocksBelowSlot, + overrideHighest: func(_ context.Context, _ types.Slot) ([]interfaces.SignedBeaconBlock, error) { + return []interfaces.SignedBeaconBlock{}, nil + }, + slot: end, + }, + { + name: "HighestSlotBlocksBelow no canonical", err: ErrNoCanonicalBlockForSlot, canon: &mockCanonicalChecker{is: false}, slot: end, @@ -218,9 +237,9 @@ func TestCanonicalBlockForSlotNonHappy(t *testing.T) { } return false, nil }}, - overrideHighest: func(_ context.Context, s types.Slot) (interfaces.SignedBeaconBlock, error) { + overrideHighest: func(_ context.Context, s types.Slot) ([]interfaces.SignedBeaconBlock, error) { slotOrderObserved = append(slotOrderObserved, s) - // this allows the mock HighestBlockBelowSlot to continue to execute now that we've recorded + // this allows the mock HighestSlotBlocksBelow to continue to execute now that we've recorded // the slot in our channel return nil, errFallThroughOverride }, @@ -236,9 +255,9 @@ func TestCanonicalBlockForSlotNonHappy(t *testing.T) { } return false, nil }}, - overrideHighest: func(_ context.Context, s types.Slot) (interfaces.SignedBeaconBlock, error) { + overrideHighest: func(_ context.Context, s types.Slot) ([]interfaces.SignedBeaconBlock, error) { slotOrderObserved = append(slotOrderObserved, s) - // this allows the mock HighestBlockBelowSlot to continue to execute now that we've recorded + // this allows the mock HighestSlotBlocksBelow to continue to execute now that we've recorded // the slot in our channel return nil, errFallThroughOverride }, @@ -254,7 +273,7 @@ func TestCanonicalBlockForSlotNonHappy(t *testing.T) { canon = c.canon } ch := &CanonicalHistory{h: hist, cc: canon, cs: hist} - hist.overrideHighestBlockBelowSlot = c.overrideHighest + hist.overrideHighestSlotBlocksBelow = c.overrideHighest r, _, err := ch.BlockForSlot(ctx, c.slot) if c.err == nil { require.NoError(t, err) @@ -262,7 +281,7 @@ func TestCanonicalBlockForSlotNonHappy(t *testing.T) { require.ErrorIs(t, err, c.err) } if len(c.slotOrderExpected) > 0 { - require.Equal(t, len(c.slotOrderExpected), len(slotOrderObserved), "HighestBlockBelowSlot not called the expected number of times") + require.Equal(t, len(c.slotOrderExpected), len(slotOrderObserved), "HighestSlotBlocksBelow not called the expected number of times") for i := range c.slotOrderExpected { require.Equal(t, c.slotOrderExpected[i], slotOrderObserved[i]) } diff --git a/beacon-chain/state/stategen/migrate.go b/beacon-chain/state/stategen/migrate.go index d8b8f8c2f4..ca444e5833 100644 --- a/beacon-chain/state/stategen/migrate.go +++ b/beacon-chain/state/stategen/migrate.go @@ -55,11 +55,16 @@ func (s *State) MigrateToCold(ctx context.Context, fRoot [32]byte) error { aRoot = cached.root aState = cached.state } else { - blk, err := s.beaconDB.HighestBlockBelowSlot(ctx, slot) + blks, err := s.beaconDB.HighestSlotBlocksBelow(ctx, slot) if err != nil { return err } - missingRoot, err := blk.Block().HashTreeRoot() + // Given the block has been finalized, the db should not have more than one block in a given slot. + // We should error out when this happens. + if len(blks) != 1 { + return errUnknownBlock + } + missingRoot, err := blks[0].Block().HashTreeRoot() if err != nil { return err } diff --git a/beacon-chain/state/stategen/mock_test.go b/beacon-chain/state/stategen/mock_test.go index c507abefdc..73711fb1fe 100644 --- a/beacon-chain/state/stategen/mock_test.go +++ b/beacon-chain/state/stategen/mock_test.go @@ -74,14 +74,14 @@ type mockHistorySpec struct { } type mockHistory struct { - blocks map[[32]byte]interfaces.SignedBeaconBlock - slotMap map[types.Slot][32]byte - slotIndex slotList - canonical map[[32]byte]bool - states map[[32]byte]state.BeaconState - hiddenStates map[[32]byte]state.BeaconState - current types.Slot - overrideHighestBlockBelowSlot func(context.Context, types.Slot) (interfaces.SignedBeaconBlock, error) + blocks map[[32]byte]interfaces.SignedBeaconBlock + slotMap map[types.Slot][32]byte + slotIndex slotList + canonical map[[32]byte]bool + states map[[32]byte]state.BeaconState + hiddenStates map[[32]byte]state.BeaconState + current types.Slot + overrideHighestSlotBlocksBelow func(context.Context, types.Slot) ([]interfaces.SignedBeaconBlock, error) } type slotList []types.Slot @@ -98,11 +98,11 @@ func (m slotList) Swap(i, j int) { m[i], m[j] = m[j], m[i] } -var errFallThroughOverride = errors.New("override yielding control back to real HighestBlockBelowSlot") +var errFallThroughOverride = errors.New("override yielding control back to real HighestSlotBlocksBelow") -func (m *mockHistory) HighestBlockBelowSlot(_ context.Context, slot types.Slot) (interfaces.SignedBeaconBlock, error) { - if m.overrideHighestBlockBelowSlot != nil { - s, err := m.overrideHighestBlockBelowSlot(context.Background(), slot) +func (m *mockHistory) HighestSlotBlocksBelow(_ context.Context, slot types.Slot) ([]interfaces.SignedBeaconBlock, error) { + if m.overrideHighestSlotBlocksBelow != nil { + s, err := m.overrideHighestSlotBlocksBelow(context.Background(), slot) if !errors.Is(err, errFallThroughOverride) { return s, err } @@ -115,10 +115,10 @@ func (m *mockHistory) HighestBlockBelowSlot(_ context.Context, slot types.Slot) } for _, s := range m.slotIndex { if s < slot { - return m.blocks[m.slotMap[s]], nil + return []interfaces.SignedBeaconBlock{m.blocks[m.slotMap[s]]}, nil } } - return nil, nil + return []interfaces.SignedBeaconBlock{}, nil } var errGenesisBlockNotFound = errors.New("canonical genesis block not found in db") diff --git a/beacon-chain/state/stategen/replayer.go b/beacon-chain/state/stategen/replayer.go index 4016ea5c28..04f2e78b9f 100644 --- a/beacon-chain/state/stategen/replayer.go +++ b/beacon-chain/state/stategen/replayer.go @@ -27,7 +27,7 @@ const ( // HistoryAccessor describes the minimum set of database methods needed to support the ReplayerBuilder. type HistoryAccessor interface { - HighestBlockBelowSlot(ctx context.Context, slot types.Slot) (interfaces.SignedBeaconBlock, error) + HighestSlotBlocksBelow(ctx context.Context, slot types.Slot) ([]interfaces.SignedBeaconBlock, error) GenesisBlock(ctx context.Context) (interfaces.SignedBeaconBlock, error) Block(ctx context.Context, blockRoot [32]byte) (interfaces.SignedBeaconBlock, error) StateOrError(ctx context.Context, blockRoot [32]byte) (state.BeaconState, error)