Repository: libp2p/go-libp2p
Pull request #1652 (2022-07-09 22:31:20)

examples: fix unresponsive pubsub chat example
Update ui for example
examples/pubsub/chat/ui.go:125

Remove unneeded lock on peersList textview.
Clear method already has lock.

```golang
// Clear removes all text from the buffer.
func (t *TextView) Clear() *TextView {
	t.Lock()
	defer t.Unlock()

	t.clear()
	return t
}
```


Patch:
diff --git a/examples/pubsub/chat/ui.go b/examples/pubsub/chat/ui.go
index 165f6768..51725bd1 100644
--- a/examples/pubsub/chat/ui.go
+++ b/examples/pubsub/chat/ui.go
@@ -121,10 +121,8 @@ func (ui *ChatUI) end() {
 func (ui *ChatUI) refreshPeers() {
 	peers := ui.cr.ListPeers()
 
-	// clear is not threadsafe so we need to take the lock.
-	ui.peersList.Lock()
+	// clear is thread-safe
 	ui.peersList.Clear()
-	ui.peersList.Unlock()
 
 	for _, p := range peers {
 		fmt.Fprintln(ui.peersList, shortID(p))
diff --git a/go.mod b/go.mod
index acb6daf1..2431b45a 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,6 @@ require (
 	github.com/hashicorp/golang-lru v0.5.4
 	github.com/ipfs/go-cid v0.2.0
 	github.com/ipfs/go-datastore v0.5.1
-	github.com/ipfs/go-ipfs-util v0.0.2
 	github.com/ipfs/go-log/v2 v2.5.1
 	github.com/jbenet/go-temp-err-catcher v0.1.0
 	github.com/klauspost/compress v1.15.1
@@ -38,13 +37,13 @@ require (
 	github.com/multiformats/go-multiaddr v0.6.0
 	github.com/multiformats/go-multiaddr-dns v0.3.1
 	github.com/multiformats/go-multiaddr-fmt v0.1.0
-	github.com/multiformats/go-multihash v0.1.0
+	github.com/multiformats/go-multihash v0.2.0
 	github.com/multiformats/go-multistream v0.3.3
 	github.com/multiformats/go-varint v0.0.6
 	github.com/prometheus/client_golang v1.12.1
-	github.com/raulk/go-watchdog v1.2.0
+	github.com/raulk/go-watchdog v1.3.0
 	github.com/stretchr/testify v1.8.0
-	golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
+	golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
 )
@@ -83,7 +82,6 @@ require (
 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
 	github.com/miekg/dns v1.1.50 // indirect
 	github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
-	github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect
 	github.com/mr-tron/base58 v1.2.0 // indirect
 	github.com/multiformats/go-base32 v0.0.4 // indirect
 	github.com/multiformats/go-base36 v0.1.0 // indirect
@@ -98,7 +96,6 @@ require (
 	github.com/prometheus/client_model v0.2.0 // indirect
 	github.com/prometheus/common v0.35.0 // indirect
 	github.com/prometheus/procfs v0.7.3 // indirect
-	github.com/raulk/clock v1.1.0 // indirect
 	github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect
 	github.com/spaolacci/murmur3 v1.1.0 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
diff --git a/go.sum b/go.sum
index 4793115b..ee45c59e 100644
--- a/go.sum
+++ b/go.sum
@@ -272,7 +272,6 @@ github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46U
 github.com/ipfs/go-ds-badger v0.3.0/go.mod h1:1ke6mXNqeV8K3y5Ak2bAA0osoTfmxUdupVCGm4QUIek=
 github.com/ipfs/go-ds-leveldb v0.5.0/go.mod h1:d3XG9RUDzQ6V4SHi8+Xgj9j1XuEk1z82lquxrVbml/Q=
 github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
-github.com/ipfs/go-ipfs-util v0.0.2 h1:59Sswnk1MFaiq+VcaknX7aYEyGyGDAA73ilhEK2POp8=
 github.com/ipfs/go-ipfs-util v0.0.2/go.mod h1:CbPtkWJzjLdEcezDns2XYaehFVNXG9zrdrtMecczcsQ=
 github.com/ipfs/go-log/v2 v2.0.5/go.mod h1:eZs4Xt4ZUJQFM3DlanGhy7TkwwawCZcSByscwkWG+dw=
 github.com/ipfs/go-log/v2 v2.3.0/go.mod h1:QqGoj30OTpnKaG/LKTGTxoP2mmQtjVMEnK72gynbe/g=
@@ -410,7 +409,6 @@ github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUM
 github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU=
 github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc=
 github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s=
-github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g=
 github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
 github.com/minio/sha256-simd v0.0.0-20190131020904-2d45a736cd16/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
 github.com/minio/sha256-simd v0.0.0-20190328051042-05b4dd3047e5/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
@@ -461,8 +459,8 @@ github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa
 github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc=
 github.com/multiformats/go-multihash v0.0.14/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc=
 github.com/multiformats/go-multihash v0.0.15/go.mod h1:D6aZrWNLFTV/ynMpKsNtB40mJzmCl4jb1alC0OvHiHg=
-github.com/multiformats/go-multihash v0.1.0 h1:CgAgwqk3//SVEw3T+6DqI4mWMyRuDwZtOWcJT0q9+EA=
-github.com/multiformats/go-multihash v0.1.0/go.mod h1:RJlXsxt6vHGaia+S8We0ErjhojtKzPP2AH4+kYM7k84=
+github.com/multiformats/go-multihash v0.2.0 h1:oytJb9ZA1OUW0r0f9ea18GiaPOo4SXyc7p2movyUuo4=
+github.com/multiformats/go-multihash v0.2.0/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc=
 github.com/multiformats/go-multistream v0.3.3 h1:d5PZpjwRgVlbwfdTDjife7XszfZd8KYWfROYFlGcR8o=
 github.com/multiformats/go-multistream v0.3.3/go.mod h1:ODRoqamLUsETKS9BNcII4gcRsJBU5VAwRIv7O39cEXg=
 github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
@@ -528,10 +526,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
 github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
 github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/raulk/clock v1.1.0 h1:dpb29+UKMbLqiU/jqIJptgLR1nn23HLgMY0sTCDza5Y=
-github.com/raulk/clock v1.1.0/go.mod h1:3MpVxdZ/ODBQDxbN+kzshf5OSZwPjtMDx6BBXBmOeY0=
-github.com/raulk/go-watchdog v1.2.0 h1:konN75pw2BMmZ+AfuAm5rtFsWcJpKF3m02rKituuXNo=
-github.com/raulk/go-watchdog v1.2.0/go.mod h1:lzSbAl5sh4rtI8tYHU01BWIDzgzqaQLj6RcA1i4mlqI=
+github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
+github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -656,8 +652,8 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP
 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
-golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
+golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
diff --git a/p2p/discovery/mdns/mdns.go b/p2p/discovery/mdns/mdns.go
index a84cf807..cbae29a2 100644
--- a/p2p/discovery/mdns/mdns.go
+++ b/p2p/discovery/mdns/mdns.go
@@ -176,6 +176,9 @@ func (s *mdnsService) startResolver(ctx context.Context) {
 				continue
 			}
 			for _, info := range infos {
+				if info.ID == s.host.ID() {
+					continue
+				}
 				go s.notifee.HandlePeerFound(info)
 			}
 		}
diff --git a/p2p/host/routed/routed.go b/p2p/host/routed/routed.go
index 857b5378..e783ebec 100644
--- a/p2p/host/routed/routed.go
+++ b/p2p/host/routed/routed.go
@@ -46,9 +46,12 @@ func Wrap(h host.Host, r Routing) *RoutedHost {
 // RoutedHost's Connect differs in that if the host has no addresses for a
 // given peer, it will use its routing system to try to find some.
 func (rh *RoutedHost) Connect(ctx context.Context, pi peer.AddrInfo) error {
-	// first, check if we're already connected.
-	if rh.Network().Connectedness(pi.ID) == network.Connected {
-		return nil
+	// first, check if we're already connected unless force direct dial.
+	forceDirect, _ := network.GetForceDirectDial(ctx)
+	if !forceDirect {
+		if rh.Network().Connectedness(pi.ID) == network.Connected {
+			return nil
+		}
 	}
 
 	// if we were given some addresses, keep + use them.
diff --git a/p2p/net/connmgr/watchdog_cgo.go b/p2p/net/connmgr/watchdog_cgo.go
index b048111f..526740c5 100644
--- a/p2p/net/connmgr/watchdog_cgo.go
+++ b/p2p/net/connmgr/watchdog_cgo.go
@@ -1,5 +1,5 @@
-//go:build cgo
-// +build cgo
+//go:build cgo && !nowatchdog
+// +build cgo,!nowatchdog
 
 package connmgr
 
diff --git a/p2p/net/connmgr/watchdog_no_cgo.go b/p2p/net/connmgr/watchdog_no_cgo.go
index fb83c286..2336c3d5 100644
--- a/p2p/net/connmgr/watchdog_no_cgo.go
+++ b/p2p/net/connmgr/watchdog_no_cgo.go
@@ -1,5 +1,5 @@
-//go:build !cgo
-// +build !cgo
+//go:build !cgo || nowatchdog
+// +build !cgo nowatchdog
 
 package connmgr
 
diff --git a/p2p/protocol/holepunch/svc.go b/p2p/protocol/holepunch/svc.go
index f7ca7c4a..3bfff1dd 100644
--- a/p2p/protocol/holepunch/svc.go
+++ b/p2p/protocol/holepunch/svc.go
@@ -242,7 +242,7 @@ func (s *Service) handleNewStream(str network.Stream) {
 	rtt, addrs, err := s.incomingHolePunch(str)
 	if err != nil {
 		s.tracer.ProtocolError(rp, err)
-		log.Debugw("error handling holepunching stream from", rp, "error", err)
+		log.Debugw("error handling holepunching stream from", "peer", rp, "error", err)
 		str.Reset()
 		return
 	}
diff --git a/p2p/protocol/ping/ping.go b/p2p/protocol/ping/ping.go
index 3f19c21e..2a977de4 100644
--- a/p2p/protocol/ping/ping.go
+++ b/p2p/protocol/ping/ping.go
@@ -3,11 +3,13 @@ package ping
 import (
 	"bytes"
 	"context"
+	"crypto/rand"
+	"encoding/binary"
 	"errors"
 	"io"
+	mrand "math/rand"
 	"time"
 
-	u "github.com/ipfs/go-ipfs-util"
 	logging "github.com/ipfs/go-log/v2"
 	pool "github.com/libp2p/go-buffer-pool"
 	"github.com/libp2p/go-libp2p-core/host"
@@ -120,6 +122,14 @@ func Ping(ctx context.Context, h host.Host, p peer.ID) <-chan Result {
 		return pingError(err)
 	}
 
+	b := make([]byte, 8)
+	if _, err := rand.Read(b); err != nil {
+		log.Errorf("failed to get cryptographic random: %s", err)
+		s.Reset()
+		return pingError(err)
+	}
+	ra := mrand.New(mrand.NewSource(int64(binary.BigEndian.Uint64(b))))
+
 	ctx, cancel := context.WithCancel(ctx)
 
 	out := make(chan Result)
@@ -129,7 +139,7 @@ func Ping(ctx context.Context, h host.Host, p peer.ID) <-chan Result {
 
 		for ctx.Err() == nil {
 			var res Result
-			res.RTT, res.Error = ping(s)
+			res.RTT, res.Error = ping(s, ra)
 
 			// canceled, ignore everything.
 			if ctx.Err() != nil {
@@ -157,7 +167,7 @@ func Ping(ctx context.Context, h host.Host, p peer.ID) <-chan Result {
 	return out
 }
 
-func ping(s network.Stream) (time.Duration, error) {
+func ping(s network.Stream, randReader io.Reader) (time.Duration, error) {
 	if err := s.Scope().ReserveMemory(2*PingSize, network.ReservationPriorityAlways); err != nil {
 		log.Debugf("error reserving memory for ping stream: %s", err)
 		s.Reset()
@@ -168,19 +178,19 @@ func ping(s network.Stream) (time.Duration, error) {
 	buf := pool.Get(PingSize)
 	defer pool.Put(buf)
 
-	u.NewTimeSeededRand().Read(buf)
+	if _, err := io.ReadFull(randReader, buf); err != nil {
+		return 0, err
+	}
 
 	before := time.Now()
-	_, err := s.Write(buf)
-	if err != nil {
+	if _, err := s.Write(buf); err != nil {
 		return 0, err
 	}
 
 	rbuf := pool.Get(PingSize)
 	defer pool.Put(rbuf)
 
-	_, err = io.ReadFull(s, rbuf)
-	if err != nil {
+	if _, err := io.ReadFull(s, rbuf); err != nil {
 		return 0, err
 	}
 
diff --git a/p2p/security/noise/handshake.go b/p2p/security/noise/handshake.go
index 59d1eebe..504a2b15 100644
--- a/p2p/security/noise/handshake.go
+++ b/p2p/security/noise/handshake.go
@@ -5,10 +5,12 @@ import (
 	"crypto/rand"
 	"encoding/binary"
 	"fmt"
+	"hash"
 	"os"
 	"runtime/debug"
 	"time"
 
+	"github.com/minio/sha256-simd"
 	"golang.org/x/crypto/chacha20poly1305"
 
 	"github.com/libp2p/go-libp2p/p2p/security/noise/pb"
@@ -25,8 +27,15 @@ import (
 // our libp2p identity key.
 const payloadSigPrefix = "noise-libp2p-static-key:"
 
+type minioSHAFn struct{}
+
+func (h minioSHAFn) Hash() hash.Hash  { return sha256.New() }
+func (h minioSHAFn) HashName() string { return "SHA256" }
+
+var shaHashFn noise.HashFunc = minioSHAFn{}
+
 // All noise session share a fixed cipher suite
-var cipherSuite = noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, noise.HashSHA256)
+var cipherSuite = noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, shaHashFn)
 
 // runHandshake exchanges handshake messages with the remote peer to establish
 // a noise-libp2p session. It blocks until the handshake completes or fails.
@@ -48,6 +57,7 @@ func (s *secureSession) runHandshake(ctx context.Context) (err error) {
 		Pattern:       noise.HandshakeXX,
 		Initiator:     s.initiator,
 		StaticKeypair: kp,
+		Prologue:      s.prologue,
 	}
 
 	hs, err := noise.NewHandshakeState(cfg)
diff --git a/p2p/security/noise/session.go b/p2p/security/noise/session.go
index 9675cc03..9770178a 100644
--- a/p2p/security/noise/session.go
+++ b/p2p/security/noise/session.go
@@ -34,11 +34,14 @@ type secureSession struct {
 
 	enc *noise.CipherState
 	dec *noise.CipherState
+
+	// noise prologue
+	prologue []byte
 }
 
 // newSecureSession creates a Noise session over the given insecureConn Conn, using
 // the libp2p identity keypair from the given Transport.
-func newSecureSession(tpt *Transport, ctx context.Context, insecure net.Conn, remote peer.ID, initiator bool) (*secureSession, error) {
+func newSecureSession(tpt *Transport, ctx context.Context, insecure net.Conn, remote peer.ID, prologue []byte, initiator bool) (*secureSession, error) {
 	s := &secureSession{
 		insecureConn:   insecure,
 		insecureReader: bufio.NewReader(insecure),
@@ -46,6 +49,7 @@ func newSecureSession(tpt *Transport, ctx context.Context, insecure net.Conn, re
 		localID:        tpt.localID,
 		localKey:       tpt.privateKey,
 		remoteID:       remote,
+		prologue:       prologue,
 	}
 
 	// the go-routine we create to run the handshake will
diff --git a/p2p/security/noise/session_transport.go b/p2p/security/noise/session_transport.go
new file mode 100644
index 00000000..b996d95b
--- /dev/null
+++ b/p2p/security/noise/session_transport.go
@@ -0,0 +1,58 @@
+package noise
+
+import (
+	"context"
+	"net"
+
+	"github.com/libp2p/go-libp2p-core/canonicallog"
+	"github.com/libp2p/go-libp2p-core/peer"
+	"github.com/libp2p/go-libp2p-core/sec"
+	manet "github.com/multiformats/go-multiaddr/net"
+)
+
+type SessionOption = func(*SessionTransport) error
+
+var _ sec.SecureTransport = &SessionTransport{}
+
+// SessionTransport can be used
+// to provide per-connection options
+type SessionTransport struct {
+	t *Transport
+	// options
+	prologue []byte
+}
+
+// SecureInbound runs the Noise handshake as the responder.
+// If p is empty, connections from any peer are accepted.
+func (i *SessionTransport) SecureInbound(ctx context.Context, insecure net.Conn, p peer.ID) (sec.SecureConn, error) {
+	c, err := newSecureSession(i.t, ctx, insecure, p, i.prologue, false)
+	if err != nil {
+		addr, maErr := manet.FromNetAddr(insecure.RemoteAddr())
+		if maErr == nil {
+			canonicallog.LogPeerStatus(100, p, addr, "handshake_failure", "noise", "err", err.Error())
+		}
+	}
+	return c, err
+}
+
+// SecureOutbound runs the Noise handshake as the initiator.
+func (i *SessionTransport) SecureOutbound(ctx context.Context, insecure net.Conn, p peer.ID) (sec.SecureConn, error) {
+	return newSecureSession(i.t, ctx, insecure, p, i.prologue, true)
+}
+
+func (t *Transport) WithSessionOptions(opts ...SessionOption) (sec.SecureTransport, error) {
+	st := &SessionTransport{t: t}
+	for _, opt := range opts {
+		if err := opt(st); err != nil {
+			return nil, err
+		}
+	}
+	return st, nil
+}
+
+func Prologue(prologue []byte) SessionOption {
+	return func(s *SessionTransport) error {
+		s.prologue = prologue
+		return nil
+	}
+}
diff --git a/p2p/security/noise/transport.go b/p2p/security/noise/transport.go
index c39da869..689a5e12 100644
--- a/p2p/security/noise/transport.go
+++ b/p2p/security/noise/transport.go
@@ -40,7 +40,7 @@ func New(privkey crypto.PrivKey) (*Transport, error) {
 // SecureInbound runs the Noise handshake as the responder.
 // If p is empty, connections from any peer are accepted.
 func (t *Transport) SecureInbound(ctx context.Context, insecure net.Conn, p peer.ID) (sec.SecureConn, error) {
-	c, err := newSecureSession(t, ctx, insecure, p, false)
+	c, err := newSecureSession(t, ctx, insecure, p, nil, false)
 	if err != nil {
 		addr, maErr := manet.FromNetAddr(insecure.RemoteAddr())
 		if maErr == nil {
@@ -52,5 +52,5 @@ func (t *Transport) SecureInbound(ctx context.Context, insecure net.Conn, p peer
 
 // SecureOutbound runs the Noise handshake as the initiator.
 func (t *Transport) SecureOutbound(ctx context.Context, insecure net.Conn, p peer.ID) (sec.SecureConn, error) {
-	return newSecureSession(t, ctx, insecure, p, true)
+	return newSecureSession(t, ctx, insecure, p, nil, true)
 }
diff --git a/p2p/security/tls/crypto.go b/p2p/security/tls/crypto.go
index 24a06939..698cccf4 100644
--- a/p2p/security/tls/crypto.go
+++ b/p2p/security/tls/crypto.go
@@ -1,6 +1,7 @@
 package libp2ptls
 
 import (
+	"crypto"
 	"crypto/ecdsa"
 	"crypto/elliptic"
 	"crypto/rand"
@@ -38,9 +39,37 @@ type Identity struct {
 	config tls.Config
 }
 
+// IdentityConfig is used to configure an Identity
+type IdentityConfig struct {
+	CertTemplate *x509.Certificate
+}
+
+// IdentityOption transforms an IdentityConfig to apply optional settings.
+type IdentityOption func(r *IdentityConfig)
+
+// WithCertTemplate specifies the template to use when generating a new certificate.
+func WithCertTemplate(template *x509.Certificate) IdentityOption {
+	return func(c *IdentityConfig) {
+		c.CertTemplate = template
+	}
+}
+
 // NewIdentity creates a new identity
-func NewIdentity(privKey ic.PrivKey) (*Identity, error) {
-	cert, err := keyToCertificate(privKey)
+func NewIdentity(privKey ic.PrivKey, opts ...IdentityOption) (*Identity, error) {
+	config := IdentityConfig{}
+	for _, opt := range opts {
+		opt(&config)
+	}
+
+	var err error
+	if config.CertTemplate == nil {
+		config.CertTemplate, err = certTemplate()
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	cert, err := keyToCertificate(privKey, config.CertTemplate)
 	if err != nil {
 		return nil, err
 	}
@@ -167,60 +196,79 @@ func PubKeyFromCertChain(chain []*x509.Certificate) (ic.PubKey, error) {
 	return pubKey, nil
 }
 
-func keyToCertificate(sk ic.PrivKey) (*tls.Certificate, error) {
-	certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
-	if err != nil {
-		return nil, err
-	}
-
+// GenerateSignedExtension uses the provided private key to sign the public key, and returns the
+// signature within a pkix.Extension.
+// This extension is included in a certificate to cryptographically tie it to the libp2p private key.
+func GenerateSignedExtension(sk ic.PrivKey, pubKey crypto.PublicKey) (pkix.Extension, error) {
 	keyBytes, err := ic.MarshalPublicKey(sk.GetPublic())
 	if err != nil {
-		return nil, err
+		return pkix.Extension{}, err
 	}
-	certKeyPub, err := x509.MarshalPKIXPublicKey(certKey.Public())
+	certKeyPub, err := x509.MarshalPKIXPublicKey(pubKey)
 	if err != nil {
-		return nil, err
+		return pkix.Extension{}, err
 	}
 	signature, err := sk.Sign(append([]byte(certificatePrefix), certKeyPub...))
 	if err != nil {
-		return nil, err
+		return pkix.Extension{}, err
 	}
 	value, err := asn1.Marshal(signedKey{
 		PubKey:    keyBytes,
 		Signature: signature,
 	})
+	if err != nil {
+		return pkix.Extension{}, err
+	}
+
+	return pkix.Extension{Id: extensionID, Critical: extensionCritical, Value: value}, nil
+}
+
+// keyToCertificate generates a new ECDSA private key and corresponding x509 certificate.
+// The certificate includes an extension that cryptographically ties it to the provided libp2p
+// private key to authenticate TLS connections.
+func keyToCertificate(sk ic.PrivKey, certTmpl *x509.Certificate) (*tls.Certificate, error) {
+	certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		return nil, err
+	}
+
+	// after calling CreateCertificate, these will end up in Certificate.Extensions
+	extension, err := GenerateSignedExtension(sk, certKey.Public())
+	if err != nil {
+		return nil, err
+	}
+	certTmpl.ExtraExtensions = append(certTmpl.ExtraExtensions, extension)
+
+	certDER, err := x509.CreateCertificate(rand.Reader, certTmpl, certTmpl, certKey.Public(), certKey)
 	if err != nil {
 		return nil, err
 	}
+	return &tls.Certificate{
+		Certificate: [][]byte{certDER},
+		PrivateKey:  certKey,
+	}, nil
+}
 
+// certTemplate returns the template for generating an Identity's TLS certificates.
+func certTemplate() (*x509.Certificate, error) {
 	bigNum := big.NewInt(1 << 62)
 	sn, err := rand.Int(rand.Reader, bigNum)
 	if err != nil {
 		return nil, err
 	}
+
 	subjectSN, err := rand.Int(rand.Reader, bigNum)
 	if err != nil {
 		return nil, err
 	}
-	tmpl := &x509.Certificate{
+
+	return &x509.Certificate{
 		SerialNumber: sn,
 		NotBefore:    time.Now().Add(-time.Hour),
 		NotAfter:     time.Now().Add(certValidityPeriod),
 		// According to RFC 3280, the issuer field must be set,
 		// see https://datatracker.ietf.org/doc/html/rfc3280#section-4.1.2.4.
 		Subject: pkix.Name{SerialNumber: subjectSN.String()},
-		// after calling CreateCertificate, these will end up in Certificate.Extensions
-		ExtraExtensions: []pkix.Extension{
-			{Id: extensionID, Critical: extensionCritical, Value: value},
-		},
-	}
-	certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, certKey.Public(), certKey)
-	if err != nil {
-		return nil, err
-	}
-	return &tls.Certificate{
-		Certificate: [][]byte{certDER},
-		PrivateKey:  certKey,
 	}, nil
 }
 
@@ -229,7 +277,7 @@ func keyToCertificate(sk ic.PrivKey) (*tls.Certificate, error) {
 // x86->x86: AES, ARM->x86: ChaCha, x86->ARM: ChaCha and ARM->ARM: Chacha
 // This function returns true if we don't have AES hardware support, and false otherwise.
 // Thus, ARM servers will always use their own cipher suite preferences (ChaCha first),
-// and x86 servers will aways use the client's cipher suite preferences.
+// and x86 servers will always use the client's cipher suite preferences.
 func preferServerCipherSuites() bool {
 	// Copied from the Go TLS implementation.
 
diff --git a/version.json b/version.json
index 14e1da56..c6a304fe 100644
--- a/version.json
+++ b/version.json
@@ -1,3 +1,3 @@
 {
-  "version": "v0.21.0-rc"
+  "version": "v0.21.0"
 }


Test patch:
diff --git a/.github/workflows/interop-test.yml b/.github/workflows/interop-test.yml
new file mode 100644
index 00000000..06134982
--- /dev/null
+++ b/.github/workflows/interop-test.yml
@@ -0,0 +1,16 @@
+on: [push, pull_request]
+name: Interoperability Testing
+
+jobs:
+  run-ping-interop-cross-version:
+    uses: "libp2p/test-plans/.github/workflows/run-composition.yml@master"
+    with:
+      composition_file: "ping/_compositions/go-cross-versions.toml"
+      # NOTE: during a pull request run, github creates a merge commit referenced in `github.sha`
+      # that merge commit is not a regular commit. You won't find it with a regular `git checkout SHA` and
+      # tools like `go get repo@SHA` won't find it.
+      #
+      # As a workaround, we generate a path to the actual pull request's commit, it looks like:
+      # `github.com/external-org/go-libp2p@latest-commit-on-their-branch`
+      custom_git_target: github.com/${{ github.event.pull_request.head.repo.full_name || github.event.repository.full_name }}
+      custom_git_reference: ${{ github.event.pull_request.head.sha || github.sha }}
diff --git a/p2p/discovery/mdns/mdns_test.go b/p2p/discovery/mdns/mdns_test.go
index 8b5e000a..60a49cae 100644
--- a/p2p/discovery/mdns/mdns_test.go
+++ b/p2p/discovery/mdns/mdns_test.go
@@ -46,27 +46,6 @@ func (n *notif) GetPeers() []peer.AddrInfo {
 	return infos
 }
 
-func TestSelfDiscovery(t *testing.T) {
-	notif := &notif{}
-	hostID := setupMDNS(t, notif)
-	assert.Eventuallyf(
-		t,
-		func() bool {
-			var found bool
-			for _, info := range notif.GetPeers() {
-				if info.ID == hostID {
-					found = true
-					break
-				}
-			}
-			return found
-		},
-		5*time.Second,
-		5*time.Millisecond,
-		"expected peer to find itself",
-	)
-}
-
 func TestOtherDiscovery(t *testing.T) {
 	const n = 4
 
@@ -78,9 +57,12 @@ func TestOtherDiscovery(t *testing.T) {
 		hostIDs[i] = setupMDNS(t, notif)
 	}
 
-	containsAllHostIDs := func(ids []peer.ID) bool {
+	containsAllHostIDs := func(ids []peer.ID, currentHostID peer.ID) bool {
 		for _, id := range hostIDs {
 			var found bool
+			if currentHostID == id {
+				continue
+			}
 			for _, i := range ids {
 				if id == i {
 					found = true
@@ -97,13 +79,13 @@ func TestOtherDiscovery(t *testing.T) {
 	assert.Eventuallyf(
 		t,
 		func() bool {
-			for _, notif := range notifs {
+			for i, notif := range notifs {
 				infos := notif.GetPeers()
 				ids := make([]peer.ID, 0, len(infos))
 				for _, info := range infos {
 					ids = append(ids, info.ID)
 				}
-				if !containsAllHostIDs(ids) {
+				if !containsAllHostIDs(ids, hostIDs[i]) {
 					return false
 				}
 			}
diff --git a/p2p/host/pstoremanager/pstoremanager_test.go b/p2p/host/pstoremanager/pstoremanager_test.go
index 8a2a8f1e..72beb874 100644
--- a/p2p/host/pstoremanager/pstoremanager_test.go
+++ b/p2p/host/pstoremanager/pstoremanager_test.go
@@ -74,7 +74,6 @@ func TestReconnect(t *testing.T) {
 }
 
 func TestClose(t *testing.T) {
-	t.Parallel()
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 	eventBus := eventbus.NewBus()
@@ -98,6 +97,7 @@ func TestClose(t *testing.T) {
 	// make sure the event is sent before we close
 	select {
 	case <-sub.Out():
+		time.Sleep(100 * time.Millisecond) // make sure this event is also picked up by the pstoremanager
 	case <-time.After(5 * time.Second):
 		t.Fatalf("Hit timeout")
 	}
diff --git a/p2p/security/noise/crypto_test.go b/p2p/security/noise/crypto_test.go
index ca5125cd..87efb848 100644
--- a/p2p/security/noise/crypto_test.go
+++ b/p2p/security/noise/crypto_test.go
@@ -93,7 +93,7 @@ func TestCryptoFailsIfHandshakeIncomplete(t *testing.T) {
 	init, resp := net.Pipe()
 	_ = resp.Close()
 
-	session, _ := newSecureSession(initTransport, context.TODO(), init, "remote-peer", true)
+	session, _ := newSecureSession(initTransport, context.TODO(), init, "remote-peer", nil, true)
 	_, err := session.encrypt(nil, []byte("hi"))
 	if err == nil {
 		t.Error("expected encryption error when handshake incomplete")
diff --git a/p2p/security/noise/transport_test.go b/p2p/security/noise/transport_test.go
index e88fbe59..4ce47613 100644
--- a/p2p/security/noise/transport_test.go
+++ b/p2p/security/noise/transport_test.go
@@ -372,3 +372,57 @@ func TestReadUnencryptedFails(t *testing.T) {
 	require.Error(t, err)
 	require.Equal(t, 0, afterLen)
 }
+
+func TestPrologueMatches(t *testing.T) {
+	commonPrologue := []byte("test")
+	initTransport := newTestTransport(t, crypto.Ed25519, 2048)
+	respTransport := newTestTransport(t, crypto.Ed25519, 2048)
+
+	initConn, respConn := newConnPair(t)
+
+	done := make(chan struct{})
+
+	go func() {
+		defer close(done)
+		tpt, err := initTransport.
+			WithSessionOptions(Prologue(commonPrologue))
+		require.NoError(t, err)
+		conn, err := tpt.SecureOutbound(context.TODO(), initConn, respTransport.localID)
+		require.NoError(t, err)
+		defer conn.Close()
+	}()
+
+	tpt, err := respTransport.
+		WithSessionOptions(Prologue(commonPrologue))
+	require.NoError(t, err)
+	conn, err := tpt.SecureInbound(context.TODO(), respConn, "")
+	require.NoError(t, err)
+	defer conn.Close()
+	<-done
+}
+
+func TestPrologueDoesNotMatchFailsHandshake(t *testing.T) {
+	initPrologue, respPrologue := []byte("initPrologue"), []byte("respPrologue")
+	initTransport := newTestTransport(t, crypto.Ed25519, 2048)
+	respTransport := newTestTransport(t, crypto.Ed25519, 2048)
+
+	initConn, respConn := newConnPair(t)
+
+	done := make(chan struct{})
+
+	go func() {
+		defer close(done)
+		tpt, err := initTransport.
+			WithSessionOptions(Prologue(initPrologue))
+		require.NoError(t, err)
+		_, err = tpt.SecureOutbound(context.TODO(), initConn, respTransport.localID)
+		require.Error(t, err)
+	}()
+
+	tpt, err := respTransport.WithSessionOptions(Prologue(respPrologue))
+	require.NoError(t, err)
+
+	_, err = tpt.SecureInbound(context.TODO(), respConn, "")
+	require.Error(t, err)
+	<-done
+}
diff --git a/p2p/security/tls/crypto_test.go b/p2p/security/tls/crypto_test.go
new file mode 100644
index 00000000..e2a28bbb
--- /dev/null
+++ b/p2p/security/tls/crypto_test.go
@@ -0,0 +1,48 @@
+package libp2ptls
+
+import (
+	"crypto/x509"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewIdentityCertificates(t *testing.T) {
+	_, key := createPeer(t)
+	cn := "a.test.name"
+	email := "unittest@example.com"
+
+	t.Run("NewIdentity with default template", func(t *testing.T) {
+		// Generate an identity using the default template
+		id, err := NewIdentity(key)
+		assert.NoError(t, err)
+
+		// Extract the x509 certificate
+		x509Cert, err := x509.ParseCertificate(id.config.Certificates[0].Certificate[0])
+		assert.NoError(t, err)
+
+		// verify the common name and email are not set
+		assert.Empty(t, x509Cert.Subject.CommonName)
+		assert.Empty(t, x509Cert.EmailAddresses)
+	})
+
+	t.Run("NewIdentity with custom template", func(t *testing.T) {
+		tmpl, err := certTemplate()
+		assert.NoError(t, err)
+
+		tmpl.Subject.CommonName = cn
+		tmpl.EmailAddresses = []string{email}
+
+		// Generate an identity using the custom template
+		id, err := NewIdentity(key, WithCertTemplate(tmpl))
+		assert.NoError(t, err)
+
+		// Extract the x509 certificate
+		x509Cert, err := x509.ParseCertificate(id.config.Certificates[0].Certificate[0])
+		assert.NoError(t, err)
+
+		// verify the common name and email are set
+		assert.Equal(t, cn, x509Cert.Subject.CommonName)
+		assert.Equal(t, email, x509Cert.EmailAddresses[0])
+	})
+}
diff --git a/p2p/security/tls/transport_test.go b/p2p/security/tls/transport_test.go
index d49730f3..1d272f0f 100644
--- a/p2p/security/tls/transport_test.go
+++ b/p2p/security/tls/transport_test.go
@@ -85,12 +85,7 @@ func TestHandshakeSucceeds(t *testing.T) {
 	clientID, clientKey := createPeer(t)
 	serverID, serverKey := createPeer(t)
 
-	handshake := func(t *testing.T) {
-		clientTransport, err := New(clientKey)
-		require.NoError(t, err)
-		serverTransport, err := New(serverKey)
-		require.NoError(t, err)
-
+	handshake := func(t *testing.T, clientTransport *Transport, serverTransport *Transport) {
 		clientInsecureConn, serverInsecureConn := connect(t)
 
 		serverConnChan := make(chan sec.SecureConn)
@@ -129,15 +124,54 @@ func TestHandshakeSucceeds(t *testing.T) {
 		require.Equal(t, string(b), "foobar")
 	}
 
-	t.Run("with extension not critical", func(t *testing.T) {
-		handshake(t)
+	// Use standard transports with default TLS configuration
+	clientTransport, err := New(clientKey)
+	require.NoError(t, err)
+	serverTransport, err := New(serverKey)
+	require.NoError(t, err)
+
+	t.Run("standard TLS with extension not critical", func(t *testing.T) {
+		handshake(t, clientTransport, serverTransport)
+	})
+
+	t.Run("standard TLS with extension critical", func(t *testing.T) {
+		extensionCritical = true
+		t.Cleanup(func() { extensionCritical = false })
+
+		handshake(t, clientTransport, serverTransport)
+	})
+
+	// Use transports with custom TLS certificates
+
+	// override client identity to use a custom certificate
+	clientCertTmlp, err := certTemplate()
+	require.NoError(t, err)
+
+	clientCertTmlp.Subject.CommonName = "client.test.name"
+	clientCertTmlp.EmailAddresses = []string{"client-unittest@example.com"}
+
+	clientTransport.identity, err = NewIdentity(clientKey, WithCertTemplate(clientCertTmlp))
+	require.NoError(t, err)
+
+	// override server identity to use a custom certificate
+	serverCertTmpl, err := certTemplate()
+	require.NoError(t, err)
+
+	serverCertTmpl.Subject.CommonName = "server.test.name"
+	serverCertTmpl.EmailAddresses = []string{"server-unittest@example.com"}
+
+	serverTransport.identity, err = NewIdentity(serverKey, WithCertTemplate(serverCertTmpl))
+	require.NoError(t, err)
+
+	t.Run("custom TLS with extension not critical", func(t *testing.T) {
+		handshake(t, clientTransport, serverTransport)
 	})
 
-	t.Run("with extension critical", func(t *testing.T) {
+	t.Run("custom TLS with extension critical", func(t *testing.T) {
 		extensionCritical = true
 		t.Cleanup(func() { extensionCritical = false })
 
-		handshake(t)
+		handshake(t, clientTransport, serverTransport)
 	})
 }