Skip to content
Closed
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
18 changes: 11 additions & 7 deletions blocksync/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,13 @@ func (pool *BlockPool) AddBlock(peerID p2p.ID, block *types.Block, extCommit *ty
}

pool.mtx.Lock()
defer pool.mtx.Unlock()
var sendErr error
defer func() {
pool.mtx.Unlock()
if sendErr != nil {
pool.sendError(sendErr, peerID)
}
}()

requester := pool.requesters[block.Height]
if requester == nil {
Expand All @@ -375,19 +381,17 @@ func (pool *BlockPool) AddBlock(peerID p2p.ID, block *types.Block, extCommit *ty
// can't punish it. But if the peer sent us a block we clearly didn't
// request, we disconnect.
if block.Height > pool.height || block.Height < pool.startHeight {
err := fmt.Errorf("peer sent us block #%d we didn't expect (current height: %d, start height: %d)",
sendErr = fmt.Errorf("peer sent us block #%d we didn't expect (current height: %d, start height: %d)",
block.Height, pool.height, pool.startHeight)
pool.sendError(err, peerID)
return err
return sendErr
}

return fmt.Errorf("got an already committed block #%d (possibly from the slow peer %s)", block.Height, peerID)
}

if !requester.setBlock(block, extCommit, peerID) {
err := fmt.Errorf("requested block #%d from %v, not %s", block.Height, requester.requestedFrom(), peerID)
pool.sendError(err, peerID)
return err
sendErr = fmt.Errorf("requested block #%d from %v, not %s", block.Height, requester.requestedFrom(), peerID)
return sendErr
}

pool.numPending.Add(-1)
Expand Down
40 changes: 40 additions & 0 deletions blocksync/pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,46 @@ func TestBlockPoolMaxPeerHeightRefreshesOnPopRequest(t *testing.T) {
"peer B must contribute to maxPeerHeight once pool.height reaches its base")
}

// TestAddBlockDoesNotDeadlockOnSendError is a regression test for AddBlock
// holding pool.mtx while calling sendError on an unbuffered channel.
func TestAddBlockDoesNotDeadlockOnSendError(t *testing.T) {
requestsCh := make(chan BlockRequest, 10)
errorsCh := make(chan peerError) // unbuffered: keeps AddBlock blocked in sendError

pool := NewBlockPool(1, requestsCh, errorsCh, time.Second)
pool.SetLogger(log.TestingLogger())
require.NoError(t, pool.Start())
t.Cleanup(func() { _ = pool.Stop() })

pool.mtx.Lock()
req := newBPRequester(pool, 1)
req.peerID = "A"
pool.requesters[1] = req
pool.mtx.Unlock()

block := &types.Block{Header: types.Header{Height: 1}, LastCommit: &types.Commit{}}
extCommit := &types.ExtendedCommit{Height: 1}

// "B" did not request the block; setBlock fails → sendError while holding pool.mtx.
go func() { _ = pool.AddBlock("B", block, extCommit, 123) }()
time.Sleep(50 * time.Millisecond)

heightDone := make(chan struct{})
go func() {
pool.Height()
close(heightDone)
}()

select {
case <-heightDone:
<-errorsCh
case <-time.After(500 * time.Millisecond):
<-errorsCh
<-heightDone
t.Fatal("deadlock: AddBlock held pool.mtx while blocked in sendError")
}
}

func TestBlockPoolHasPendingRequestFrom(t *testing.T) {
requestsCh := make(chan BlockRequest, 10)
errorsCh := make(chan peerError, 10)
Expand Down
Loading