diff --git a/cmd/devp2p/internal/ethtest/helpers.go b/cmd/devp2p/internal/ethtest/helpers.go
index d99376124d2f156dd03de0402c624ca18a07974b..a9a213f3371de17f945a3edf962aecc9e4d93e87 100644
--- a/cmd/devp2p/internal/ethtest/helpers.go
+++ b/cmd/devp2p/internal/ethtest/helpers.go
@@ -633,3 +633,106 @@ func (s *Suite) maliciousStatus(conn *Conn) error {
 		return fmt.Errorf("expected disconnect, got: %s", pretty.Sdump(msg))
 	}
 }
+
+func (s *Suite) hashAnnounce(isEth66 bool) error {
+	// create connections
+	sendConn, recvConn, err := s.createSendAndRecvConns(isEth66)
+	if err != nil {
+		return fmt.Errorf("failed to create connections: %v", err)
+	}
+	defer sendConn.Close()
+	defer recvConn.Close()
+	if err := sendConn.peer(s.chain, nil); err != nil {
+		return fmt.Errorf("peering failed: %v", err)
+	}
+	if err := recvConn.peer(s.chain, nil); err != nil {
+		return fmt.Errorf("peering failed: %v", err)
+	}
+	// create NewBlockHashes announcement
+	nextBlock := s.fullChain.blocks[s.chain.Len()]
+	newBlockHash := &NewBlockHashes{
+		{Hash: nextBlock.Hash(), Number: nextBlock.Number().Uint64()},
+	}
+
+	if err := sendConn.Write(newBlockHash); err != nil {
+		return fmt.Errorf("failed to write to connection: %v", err)
+	}
+	if isEth66 {
+		// expect GetBlockHeaders request, and respond
+		id, msg := sendConn.Read66()
+		switch msg := msg.(type) {
+		case GetBlockHeaders:
+			blockHeaderReq := msg
+			if blockHeaderReq.Amount != 1 {
+				return fmt.Errorf("unexpected number of block headers requested: %v", blockHeaderReq.Amount)
+			}
+			if blockHeaderReq.Origin.Hash != nextBlock.Hash() {
+				return fmt.Errorf("unexpected block header requested: %v", pretty.Sdump(blockHeaderReq))
+			}
+			resp := &eth.BlockHeadersPacket66{
+				RequestId: id,
+				BlockHeadersPacket: eth.BlockHeadersPacket{
+					nextBlock.Header(),
+				},
+			}
+			if err := sendConn.Write66(resp, BlockHeaders{}.Code()); err != nil {
+				return fmt.Errorf("failed to write to connection: %v", err)
+			}
+		default:
+			return fmt.Errorf("unexpected %s", pretty.Sdump(msg))
+		}
+	} else {
+		// expect GetBlockHeaders request, and respond
+		switch msg := sendConn.Read().(type) {
+		case *GetBlockHeaders:
+			blockHeaderReq := *msg
+			if blockHeaderReq.Amount != 1 {
+				return fmt.Errorf("unexpected number of block headers requested: %v", blockHeaderReq.Amount)
+			}
+			if blockHeaderReq.Origin.Hash != nextBlock.Hash() {
+				return fmt.Errorf("unexpected block header requested: %v", pretty.Sdump(blockHeaderReq))
+			}
+			if err := sendConn.Write(&BlockHeaders{nextBlock.Header()}); err != nil {
+				return fmt.Errorf("failed to write to connection: %v", err)
+			}
+		default:
+			return fmt.Errorf("unexpected %s", pretty.Sdump(msg))
+		}
+	}
+	// wait for block announcement
+	msg := recvConn.readAndServe(s.chain, timeout)
+	switch msg := msg.(type) {
+	case *NewBlockHashes:
+		hashes := *msg
+		if len(hashes) != 1 {
+			return fmt.Errorf("unexpected new block hash announcement: wanted 1 announcement, got %d", len(hashes))
+		}
+		if nextBlock.Hash() != hashes[0].Hash {
+			return fmt.Errorf("unexpected block hash announcement, wanted %v, got %v", nextBlock.Hash(),
+				hashes[0].Hash)
+		}
+	case *NewBlock:
+		// node should only propagate NewBlock without having requested the body if the body is empty
+		nextBlockBody := nextBlock.Body()
+		if len(nextBlockBody.Transactions) != 0 || len(nextBlockBody.Uncles) != 0 {
+			return fmt.Errorf("unexpected non-empty new block propagated: %s", pretty.Sdump(msg))
+		}
+		if msg.Block.Hash() != nextBlock.Hash() {
+			return fmt.Errorf("mismatched hash of propagated new block: wanted %v, got %v",
+				nextBlock.Hash(), msg.Block.Hash())
+		}
+		// check to make sure header matches header that was sent to the node
+		if !reflect.DeepEqual(nextBlock.Header(), msg.Block.Header()) {
+			return fmt.Errorf("incorrect header received: wanted %v, got %v", nextBlock.Header(), msg.Block.Header())
+		}
+	default:
+		return fmt.Errorf("unexpected: %s", pretty.Sdump(msg))
+	}
+	// confirm node imported block
+	if err := s.waitForBlockImport(recvConn, nextBlock, isEth66); err != nil {
+		return fmt.Errorf("error waiting for node to import new block: %v", err)
+	}
+	// update the chain
+	s.chain.blocks = append(s.chain.blocks, nextBlock)
+	return nil
+}
diff --git a/cmd/devp2p/internal/ethtest/suite.go b/cmd/devp2p/internal/ethtest/suite.go
index ad832dddd2dea9c524e551b258ded14274ead9d9..491bcda7e7c2b4575860533921cb4f7658a4bc8e 100644
--- a/cmd/devp2p/internal/ethtest/suite.go
+++ b/cmd/devp2p/internal/ethtest/suite.go
@@ -70,6 +70,8 @@ func (s *Suite) AllEthTests() []utesting.Test {
 		{Name: "TestLargeAnnounce66", Fn: s.TestLargeAnnounce66},
 		{Name: "TestOldAnnounce", Fn: s.TestOldAnnounce},
 		{Name: "TestOldAnnounce66", Fn: s.TestOldAnnounce66},
+		{Name: "TestBlockHashAnnounce", Fn: s.TestBlockHashAnnounce},
+		{Name: "TestBlockHashAnnounce66", Fn: s.TestBlockHashAnnounce66},
 		// malicious handshakes + status
 		{Name: "TestMaliciousHandshake", Fn: s.TestMaliciousHandshake},
 		{Name: "TestMaliciousStatus", Fn: s.TestMaliciousStatus},
@@ -93,6 +95,7 @@ func (s *Suite) EthTests() []utesting.Test {
 		{Name: "TestBroadcast", Fn: s.TestBroadcast},
 		{Name: "TestLargeAnnounce", Fn: s.TestLargeAnnounce},
 		{Name: "TestOldAnnounce", Fn: s.TestOldAnnounce},
+		{Name: "TestBlockHashAnnounce", Fn: s.TestBlockHashAnnounce},
 		{Name: "TestMaliciousHandshake", Fn: s.TestMaliciousHandshake},
 		{Name: "TestMaliciousStatus", Fn: s.TestMaliciousStatus},
 		{Name: "TestTransaction", Fn: s.TestTransaction},
@@ -112,6 +115,7 @@ func (s *Suite) Eth66Tests() []utesting.Test {
 		{Name: "TestBroadcast66", Fn: s.TestBroadcast66},
 		{Name: "TestLargeAnnounce66", Fn: s.TestLargeAnnounce66},
 		{Name: "TestOldAnnounce66", Fn: s.TestOldAnnounce66},
+		{Name: "TestBlockHashAnnounce66", Fn: s.TestBlockHashAnnounce66},
 		{Name: "TestMaliciousHandshake66", Fn: s.TestMaliciousHandshake66},
 		{Name: "TestMaliciousStatus66", Fn: s.TestMaliciousStatus66},
 		{Name: "TestTransaction66", Fn: s.TestTransaction66},
@@ -580,6 +584,22 @@ func (s *Suite) TestOldAnnounce66(t *utesting.T) {
 	}
 }
 
+// TestBlockHashAnnounce sends a new block hash announcement and expects
+// the node to perform a `GetBlockHeaders` request.
+func (s *Suite) TestBlockHashAnnounce(t *utesting.T) {
+	if err := s.hashAnnounce(eth65); err != nil {
+		t.Fatalf("block hash announcement failed: %v", err)
+	}
+}
+
+// TestBlockHashAnnounce66 sends a new block hash announcement and expects
+// the node to perform a `GetBlockHeaders` request.
+func (s *Suite) TestBlockHashAnnounce66(t *utesting.T) {
+	if err := s.hashAnnounce(eth66); err != nil {
+		t.Fatalf("block hash announcement failed: %v", err)
+	}
+}
+
 // TestMaliciousHandshake tries to send malicious data during the handshake.
 func (s *Suite) TestMaliciousHandshake(t *utesting.T) {
 	if err := s.maliciousHandshakes(t, eth65); err != nil {