Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions state/protocol/badger/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,16 +326,17 @@ func (s *Snapshot) descendants(blockID flow.Identifier) ([]flow.Identifier, erro
var descendantIDs flow.IdentifierList
err := operation.RetrieveBlockChildren(s.state.db.Reader(), blockID, &descendantIDs)
if err != nil {
if !errors.Is(err, storage.ErrNotFound) {
return nil, fmt.Errorf("could not get children of block %v: %w", blockID, err)
if errors.Is(err, storage.ErrNotFound) {
// The low-level storage returns `storage.ErrNotFound` in two cases:
// 1. the block/collection is unknown
// 2. the block/collection is known but no children have been indexed yet
// By contract of the constructor, the blockID must correspond to a known collection in the database.
// A snapshot with s.err == nil is only created for known blocks. Hence, only case 2 is
// possible here, and we just return an empty list.
return []flow.Identifier{}, nil
}

// The low-level storage returns `storage.ErrNotFound` in two cases:
// 1. the block/collection is unknown
// 2. the block/collection is known but no children have been indexed yet
// By contract of the constructor, the blockID must correspond to a known collection in the database.
// A snapshot with s.err == nil is only created for known blocks. Hence, only case 2 is
// possible here, and we just return an empty list.
return nil, fmt.Errorf("could not get children of block %v: %w", blockID, err)
}

for _, child := range descendantIDs {
Expand Down
52 changes: 52 additions & 0 deletions state/protocol/badger/snapshot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,58 @@ func TestSnapshot_Descendants(t *testing.T) {
})
}

// TestSnapshot_DescendantsWithLeafNodes tests that Descendants() correctly handles
// pending blocks that have no children (leaf nodes). This is a regression test for
// a bug where the descendants() helper method would not return early when a block
// had no children, potentially causing incorrect behavior.
//
// Test structure:
//
// ↙ B (no children)
// A (finalized)
// ↖ C (no children)
//
// snapshot.Descendants has to return [B, C].
func TestSnapshot_DescendantsWithLeafNodes(t *testing.T) {
participants := unittest.IdentityListFixture(5, unittest.WithAllRoles())
rootSnapshot := unittest.RootSnapshotFixture(participants)
rootProtocolStateID := getRootProtocolStateID(t, rootSnapshot)
head, err := rootSnapshot.Head()
require.NoError(t, err)
util.RunWithFullProtocolState(t, rootSnapshot, func(db storage.DB, state *bprotocol.ParticipantState) {
// Track used views to prevent byzantine scenarios
usedViews := make(map[uint64]struct{})
usedViews[head.View] = struct{}{}

// Create two pending blocks (B and C) as direct children of the finalized block A.
// Neither B nor C have any children, making them leaf nodes.
// This tests that descendants() correctly returns an empty list when
// RetrieveBlockChildren returns storage.ErrNotFound for these leaf blocks.
blockB := unittest.BlockWithParentAndPayloadAndUniqueView(
head,
unittest.PayloadFixture(unittest.WithProtocolStateID(rootProtocolStateID)),
usedViews,
)
err := state.Extend(context.Background(), unittest.ProposalFromBlock(blockB))
require.NoError(t, err)

blockC := unittest.BlockWithParentAndPayloadAndUniqueView(
head,
unittest.PayloadFixture(unittest.WithProtocolStateID(rootProtocolStateID)),
usedViews,
)
err = state.Extend(context.Background(), unittest.ProposalFromBlock(blockC))
require.NoError(t, err)

expectedBlocks := []flow.Identifier{blockB.ID(), blockC.ID()}

// Query descendants of the finalized block
pendingBlocks, err := state.AtBlockID(head.ID()).Descendants()
require.NoError(t, err)
require.ElementsMatch(t, expectedBlocks, pendingBlocks)
})
}

func TestIdentities(t *testing.T) {
identities := unittest.IdentityListFixture(5, unittest.WithAllRoles())
rootSnapshot := unittest.RootSnapshotFixture(identities)
Expand Down
Loading