mirror of
https://source.quilibrium.com/quilibrium/ceremonyclient.git
synced 2024-12-27 09:05:18 +00:00
1084 lines
28 KiB
Go
1084 lines
28 KiB
Go
package swarm
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
mrand "math/rand"
|
|
"reflect"
|
|
"sort"
|
|
"sync"
|
|
"testing"
|
|
"testing/quick"
|
|
"time"
|
|
|
|
"github.com/libp2p/go-libp2p/core/crypto"
|
|
"github.com/libp2p/go-libp2p/core/network"
|
|
"github.com/libp2p/go-libp2p/core/peer"
|
|
"github.com/libp2p/go-libp2p/core/peerstore"
|
|
"github.com/libp2p/go-libp2p/core/sec"
|
|
"github.com/libp2p/go-libp2p/core/sec/insecure"
|
|
"github.com/libp2p/go-libp2p/core/test"
|
|
"github.com/libp2p/go-libp2p/core/transport"
|
|
"github.com/libp2p/go-libp2p/p2p/host/eventbus"
|
|
"github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem"
|
|
"github.com/libp2p/go-libp2p/p2p/muxer/yamux"
|
|
tptu "github.com/libp2p/go-libp2p/p2p/net/upgrader"
|
|
quic "github.com/libp2p/go-libp2p/p2p/transport/quic"
|
|
"github.com/libp2p/go-libp2p/p2p/transport/quicreuse"
|
|
"github.com/libp2p/go-libp2p/p2p/transport/tcp"
|
|
|
|
ma "github.com/multiformats/go-multiaddr"
|
|
manet "github.com/multiformats/go-multiaddr/net"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type mockClock struct {
|
|
*test.MockClock
|
|
}
|
|
|
|
func (m *mockClock) InstantTimer(when time.Time) InstantTimer {
|
|
return m.MockClock.InstantTimer(when)
|
|
}
|
|
|
|
func newMockClock() *mockClock {
|
|
return &mockClock{test.NewMockClock()}
|
|
}
|
|
|
|
func newPeer(t *testing.T) (crypto.PrivKey, peer.ID) {
|
|
priv, _, err := crypto.GenerateEd25519Key(rand.Reader)
|
|
require.NoError(t, err)
|
|
id, err := peer.IDFromPrivateKey(priv)
|
|
require.NoError(t, err)
|
|
return priv, id
|
|
}
|
|
|
|
func makeSwarm(t *testing.T) *Swarm {
|
|
s := makeSwarmWithNoListenAddrs(t, WithDialTimeout(1*time.Second))
|
|
if err := s.Listen(ma.StringCast("/ip4/127.0.0.1/tcp/0")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := s.Listen(ma.StringCast("/ip4/127.0.0.1/udp/0/quic")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func makeSwarmWithNoListenAddrs(t *testing.T, opts ...Option) *Swarm {
|
|
priv, id := newPeer(t)
|
|
|
|
ps, err := pstoremem.NewPeerstore()
|
|
require.NoError(t, err)
|
|
ps.AddPubKey(id, priv.GetPublic())
|
|
ps.AddPrivKey(id, priv)
|
|
t.Cleanup(func() { ps.Close() })
|
|
|
|
s, err := NewSwarm(id, ps, eventbus.NewBus(), opts...)
|
|
require.NoError(t, err)
|
|
|
|
upgrader := makeUpgrader(t, s)
|
|
var tcpOpts []tcp.Option
|
|
tcpOpts = append(tcpOpts, tcp.DisableReuseport())
|
|
tcpTransport, err := tcp.NewTCPTransport(upgrader, nil, tcpOpts...)
|
|
require.NoError(t, err)
|
|
if err := s.AddTransport(tcpTransport); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reuse, err := quicreuse.NewConnManager([32]byte{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
quicTransport, err := quic.NewTransport(priv, reuse, nil, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.AddTransport(quicTransport); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func makeUpgrader(t *testing.T, n *Swarm) transport.Upgrader {
|
|
id := n.LocalPeer()
|
|
pk := n.Peerstore().PrivKey(id)
|
|
st := insecure.NewWithIdentity(insecure.ID, id, pk)
|
|
|
|
u, err := tptu.New([]sec.SecureTransport{st}, []tptu.StreamMuxer{{ID: yamux.ID, Muxer: yamux.DefaultTransport}}, nil, nil, nil)
|
|
require.NoError(t, err)
|
|
return u
|
|
}
|
|
|
|
// makeTCPListener listens on tcp address a. On accepting a connection it notifies recvCh. Sending a message to
|
|
// channel ch will close an accepted connection
|
|
func makeTCPListener(t *testing.T, a ma.Multiaddr, recvCh chan struct{}) (list manet.Listener, ch chan struct{}) {
|
|
t.Helper()
|
|
list, err := manet.Listen(a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ch = make(chan struct{})
|
|
go func() {
|
|
for {
|
|
c, err := list.Accept()
|
|
if err != nil {
|
|
break
|
|
}
|
|
recvCh <- struct{}{}
|
|
<-ch
|
|
err = c.Close()
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
}
|
|
}()
|
|
return list, ch
|
|
}
|
|
|
|
func TestDialWorkerLoopBasic(t *testing.T) {
|
|
s1 := makeSwarm(t)
|
|
s2 := makeSwarm(t)
|
|
defer s1.Close()
|
|
defer s2.Close()
|
|
|
|
// Only pass in a single address here, otherwise we might end up with a TCP and QUIC connection dialed.
|
|
s1.Peerstore().AddAddrs(s2.LocalPeer(), []ma.Multiaddr{s2.ListenAddresses()[0]}, peerstore.PermanentAddrTTL)
|
|
|
|
reqch := make(chan dialRequest)
|
|
resch := make(chan dialResponse)
|
|
worker := newDialWorker(s1, s2.LocalPeer(), reqch, nil)
|
|
go worker.loop()
|
|
|
|
var conn *Conn
|
|
reqch <- dialRequest{ctx: context.Background(), resch: resch}
|
|
select {
|
|
case res := <-resch:
|
|
require.NoError(t, res.err)
|
|
conn = res.conn
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("dial didn't complete")
|
|
}
|
|
|
|
s, err := conn.NewStream(context.Background())
|
|
require.NoError(t, err)
|
|
s.Close()
|
|
|
|
var conn2 *Conn
|
|
reqch <- dialRequest{ctx: context.Background(), resch: resch}
|
|
select {
|
|
case res := <-resch:
|
|
require.NoError(t, res.err)
|
|
conn2 = res.conn
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("dial didn't complete")
|
|
}
|
|
|
|
// can't use require.Equal here, as this does a deep comparison
|
|
if conn != conn2 {
|
|
t.Fatalf("expecting the same connection from both dials. %s <-> %s vs. %s <-> %s", conn.LocalMultiaddr(), conn.RemoteMultiaddr(), conn2.LocalMultiaddr(), conn2.RemoteMultiaddr())
|
|
}
|
|
|
|
close(reqch)
|
|
worker.wg.Wait()
|
|
}
|
|
|
|
func TestDialWorkerLoopConcurrent(t *testing.T) {
|
|
s1 := makeSwarm(t)
|
|
s2 := makeSwarm(t)
|
|
defer s1.Close()
|
|
defer s2.Close()
|
|
|
|
s1.Peerstore().AddAddrs(s2.LocalPeer(), s2.ListenAddresses(), peerstore.PermanentAddrTTL)
|
|
|
|
reqch := make(chan dialRequest)
|
|
worker := newDialWorker(s1, s2.LocalPeer(), reqch, nil)
|
|
go worker.loop()
|
|
|
|
const dials = 100
|
|
var wg sync.WaitGroup
|
|
resch := make(chan dialResponse, dials)
|
|
for i := 0; i < dials; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
reschgo := make(chan dialResponse, 1)
|
|
reqch <- dialRequest{ctx: context.Background(), resch: reschgo}
|
|
select {
|
|
case res := <-reschgo:
|
|
resch <- res
|
|
case <-time.After(time.Minute):
|
|
resch <- dialResponse{err: errors.New("timed out!")}
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
for i := 0; i < dials; i++ {
|
|
res := <-resch
|
|
require.NoError(t, res.err)
|
|
}
|
|
|
|
t.Log("all concurrent dials done")
|
|
|
|
close(reqch)
|
|
worker.wg.Wait()
|
|
}
|
|
|
|
func TestDialWorkerLoopFailure(t *testing.T) {
|
|
s1 := makeSwarm(t)
|
|
defer s1.Close()
|
|
|
|
_, p2 := newPeer(t)
|
|
|
|
s1.Peerstore().AddAddrs(p2, []ma.Multiaddr{ma.StringCast("/ip4/11.0.0.1/tcp/1234"), ma.StringCast("/ip4/11.0.0.1/udp/1234/quic")}, peerstore.PermanentAddrTTL)
|
|
|
|
reqch := make(chan dialRequest)
|
|
resch := make(chan dialResponse)
|
|
worker := newDialWorker(s1, p2, reqch, nil)
|
|
go worker.loop()
|
|
|
|
reqch <- dialRequest{ctx: context.Background(), resch: resch}
|
|
select {
|
|
case res := <-resch:
|
|
require.Error(t, res.err)
|
|
case <-time.After(time.Minute):
|
|
t.Fatal("dial didn't complete")
|
|
}
|
|
|
|
close(reqch)
|
|
worker.wg.Wait()
|
|
}
|
|
|
|
func TestDialWorkerLoopConcurrentFailure(t *testing.T) {
|
|
s1 := makeSwarm(t)
|
|
defer s1.Close()
|
|
|
|
_, p2 := newPeer(t)
|
|
|
|
s1.Peerstore().AddAddrs(p2, []ma.Multiaddr{ma.StringCast("/ip4/11.0.0.1/tcp/1234"), ma.StringCast("/ip4/11.0.0.1/udp/1234/quic")}, peerstore.PermanentAddrTTL)
|
|
|
|
reqch := make(chan dialRequest)
|
|
worker := newDialWorker(s1, p2, reqch, nil)
|
|
go worker.loop()
|
|
|
|
const dials = 100
|
|
var errTimeout = errors.New("timed out!")
|
|
var wg sync.WaitGroup
|
|
resch := make(chan dialResponse, dials)
|
|
for i := 0; i < dials; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
reschgo := make(chan dialResponse, 1)
|
|
reqch <- dialRequest{ctx: context.Background(), resch: reschgo}
|
|
|
|
select {
|
|
case res := <-reschgo:
|
|
resch <- res
|
|
case <-time.After(time.Minute):
|
|
resch <- dialResponse{err: errTimeout}
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
for i := 0; i < dials; i++ {
|
|
res := <-resch
|
|
require.Error(t, res.err)
|
|
if res.err == errTimeout {
|
|
t.Fatal("dial response timed out")
|
|
}
|
|
}
|
|
|
|
t.Log("all concurrent dials done")
|
|
|
|
close(reqch)
|
|
worker.wg.Wait()
|
|
}
|
|
|
|
func TestDialWorkerLoopConcurrentMix(t *testing.T) {
|
|
s1 := makeSwarm(t)
|
|
s2 := makeSwarm(t)
|
|
defer s1.Close()
|
|
defer s2.Close()
|
|
|
|
s1.Peerstore().AddAddrs(s2.LocalPeer(), s2.ListenAddresses(), peerstore.PermanentAddrTTL)
|
|
s1.Peerstore().AddAddrs(s2.LocalPeer(), []ma.Multiaddr{ma.StringCast("/ip4/11.0.0.1/tcp/1234"), ma.StringCast("/ip4/11.0.0.1/udp/1234/quic")}, peerstore.PermanentAddrTTL)
|
|
|
|
reqch := make(chan dialRequest)
|
|
worker := newDialWorker(s1, s2.LocalPeer(), reqch, nil)
|
|
go worker.loop()
|
|
|
|
const dials = 100
|
|
var wg sync.WaitGroup
|
|
resch := make(chan dialResponse, dials)
|
|
for i := 0; i < dials; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
reschgo := make(chan dialResponse, 1)
|
|
reqch <- dialRequest{ctx: context.Background(), resch: reschgo}
|
|
select {
|
|
case res := <-reschgo:
|
|
resch <- res
|
|
case <-time.After(time.Minute):
|
|
resch <- dialResponse{err: errors.New("timed out!")}
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
for i := 0; i < dials; i++ {
|
|
res := <-resch
|
|
require.NoError(t, res.err)
|
|
}
|
|
|
|
t.Log("all concurrent dials done")
|
|
|
|
close(reqch)
|
|
worker.wg.Wait()
|
|
}
|
|
|
|
func TestDialWorkerLoopConcurrentFailureStress(t *testing.T) {
|
|
s1 := makeSwarm(t)
|
|
defer s1.Close()
|
|
|
|
_, p2 := newPeer(t)
|
|
|
|
var addrs []ma.Multiaddr
|
|
for i := 0; i < 16; i++ {
|
|
addrs = append(addrs, ma.StringCast(fmt.Sprintf("/ip4/11.0.0.%d/tcp/%d", i%256, 1234+i)))
|
|
}
|
|
s1.Peerstore().AddAddrs(p2, addrs, peerstore.PermanentAddrTTL)
|
|
|
|
reqch := make(chan dialRequest)
|
|
worker := newDialWorker(s1, p2, reqch, nil)
|
|
go worker.loop()
|
|
|
|
const dials = 100
|
|
var errTimeout = errors.New("timed out!")
|
|
var wg sync.WaitGroup
|
|
resch := make(chan dialResponse, dials)
|
|
for i := 0; i < dials; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
reschgo := make(chan dialResponse, 1)
|
|
reqch <- dialRequest{ctx: context.Background(), resch: reschgo}
|
|
select {
|
|
case res := <-reschgo:
|
|
t.Log("received result")
|
|
resch <- res
|
|
case <-time.After(15 * time.Second):
|
|
resch <- dialResponse{err: errTimeout}
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
for i := 0; i < dials; i++ {
|
|
res := <-resch
|
|
require.Error(t, res.err)
|
|
if res.err == errTimeout {
|
|
t.Fatal("dial response timed out")
|
|
}
|
|
}
|
|
|
|
t.Log("all concurrent dials done")
|
|
|
|
close(reqch)
|
|
worker.wg.Wait()
|
|
}
|
|
|
|
func TestDialQueueNextBatch(t *testing.T) {
|
|
addrs := make([]ma.Multiaddr, 0)
|
|
for i := 0; i < 10; i++ {
|
|
addrs = append(addrs, ma.StringCast(fmt.Sprintf("/ip4/1.2.3.4/tcp/%d", i)))
|
|
}
|
|
testcase := []struct {
|
|
name string
|
|
input []network.AddrDelay
|
|
output [][]ma.Multiaddr
|
|
}{
|
|
{
|
|
name: "next batch",
|
|
input: []network.AddrDelay{
|
|
{Addr: addrs[0], Delay: 3},
|
|
{Addr: addrs[1], Delay: 2},
|
|
{Addr: addrs[2], Delay: 1},
|
|
{Addr: addrs[3], Delay: 1},
|
|
},
|
|
output: [][]ma.Multiaddr{
|
|
{addrs[2], addrs[3]},
|
|
{addrs[1]},
|
|
{addrs[0]},
|
|
},
|
|
},
|
|
{
|
|
name: "priority queue property 2",
|
|
input: []network.AddrDelay{
|
|
{Addr: addrs[0], Delay: 5},
|
|
{Addr: addrs[1], Delay: 3},
|
|
{Addr: addrs[2], Delay: 2},
|
|
{Addr: addrs[3], Delay: 1},
|
|
{Addr: addrs[4], Delay: 1},
|
|
},
|
|
|
|
output: [][]ma.Multiaddr{
|
|
{addrs[3], addrs[4]},
|
|
{addrs[2]},
|
|
{addrs[1]},
|
|
{addrs[0]},
|
|
},
|
|
},
|
|
{
|
|
name: "updates",
|
|
input: []network.AddrDelay{
|
|
{Addr: addrs[0], Delay: 3}, // decreasing order
|
|
{Addr: addrs[1], Delay: 3},
|
|
{Addr: addrs[2], Delay: 2},
|
|
{Addr: addrs[3], Delay: 2},
|
|
{Addr: addrs[4], Delay: 1},
|
|
{Addr: addrs[0], Delay: 1}, // increasing order
|
|
{Addr: addrs[1], Delay: 1},
|
|
{Addr: addrs[2], Delay: 2},
|
|
{Addr: addrs[3], Delay: 2},
|
|
{Addr: addrs[4], Delay: 3},
|
|
},
|
|
output: [][]ma.Multiaddr{
|
|
{addrs[0], addrs[1]},
|
|
{addrs[2], addrs[3]},
|
|
{addrs[4]},
|
|
{},
|
|
},
|
|
},
|
|
{
|
|
name: "null input",
|
|
input: []network.AddrDelay{},
|
|
output: [][]ma.Multiaddr{
|
|
{},
|
|
{},
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range testcase {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
q := newDialQueue()
|
|
for i := 0; i < len(tc.input); i++ {
|
|
q.Add(tc.input[i])
|
|
}
|
|
for _, batch := range tc.output {
|
|
b := q.NextBatch()
|
|
if len(batch) != len(b) {
|
|
t.Errorf("expected %d elements got %d", len(batch), len(b))
|
|
}
|
|
sort.Slice(b, func(i, j int) bool { return b[i].Addr.String() < b[j].Addr.String() })
|
|
sort.Slice(batch, func(i, j int) bool { return batch[i].String() < batch[j].String() })
|
|
for i := 0; i < len(b); i++ {
|
|
if !b[i].Addr.Equal(batch[i]) {
|
|
log.Errorf("expected %s got %s", batch[i], b[i].Addr)
|
|
}
|
|
}
|
|
}
|
|
if q.len() != 0 {
|
|
t.Errorf("expected queue to be empty at end. got: %d", q.len())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// timedDial is a dial to a single address of the peer
|
|
type timedDial struct {
|
|
// addr is the address to dial
|
|
addr ma.Multiaddr
|
|
// delay is the delay after which this address should be dialed
|
|
delay time.Duration
|
|
// success indicates whether the dial should succeed
|
|
success bool
|
|
// failAfter is how long this dial should take to fail after it is dialed
|
|
failAfter time.Duration
|
|
}
|
|
|
|
// schedulingTestCase is used to test dialWorker loop scheduler logic
|
|
// a ranker is made according to `input` which provides the addresses to
|
|
// dial worker loop with the specified delays
|
|
// checkDialWorkerLoopScheduling then verifies that the different dial calls are
|
|
// made at the right moments
|
|
type schedulingTestCase struct {
|
|
name string
|
|
input []timedDial
|
|
maxDuration time.Duration
|
|
}
|
|
|
|
// schedulingTestCase generates a random test case
|
|
func (s schedulingTestCase) Generate(rand *mrand.Rand, size int) reflect.Value {
|
|
if size > 20 {
|
|
size = 20
|
|
}
|
|
input := make([]timedDial, size)
|
|
delays := make(map[time.Duration]struct{})
|
|
for i := 0; i < size; i++ {
|
|
input[i] = timedDial{
|
|
addr: ma.StringCast(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", i+10550)),
|
|
delay: time.Duration(mrand.Intn(100)) * 10 * time.Millisecond, // max 1 second
|
|
success: false,
|
|
failAfter: time.Duration(mrand.Intn(100)) * 10 * time.Millisecond, // max 1 second
|
|
}
|
|
delays[input[i].delay] = struct{}{}
|
|
}
|
|
successIdx := rand.Intn(size)
|
|
for {
|
|
// set a unique delay for success. This is required to test the property that
|
|
// no extra dials are made after success
|
|
d := time.Duration(rand.Intn(100)) * 10 * time.Millisecond
|
|
if _, ok := delays[d]; !ok {
|
|
input[successIdx].delay = d
|
|
input[successIdx].success = true
|
|
break
|
|
}
|
|
}
|
|
return reflect.ValueOf(schedulingTestCase{
|
|
name: "",
|
|
input: input,
|
|
maxDuration: 10 * time.Second, // not tested here
|
|
})
|
|
}
|
|
|
|
// dialState is used to track the dials for testing dialWorker ranking logic
|
|
type dialState struct {
|
|
// ch is the chan used to trigger dial failure.
|
|
ch chan struct{}
|
|
// addr is the address of the dial
|
|
addr ma.Multiaddr
|
|
// delay is the delay after which this address should be dialed
|
|
delay time.Duration
|
|
// success indicates whether the dial should succeed
|
|
success bool
|
|
// failAfter is how long this dial should take to fail after it is dialed
|
|
failAfter time.Duration
|
|
// failAt is the instant at which this dial should fail if success is false
|
|
failAt time.Time
|
|
}
|
|
|
|
// checkDialWorkerLoopScheduling verifies whether s1 dials s2 according to the
|
|
// schedule specified by the test case tc
|
|
func checkDialWorkerLoopScheduling(t *testing.T, s1, s2 *Swarm, tc schedulingTestCase) error {
|
|
t.Helper()
|
|
// failDials is used to track dials which should fail in the future
|
|
// at appropriate moment a message is sent to dialState.ch to trigger
|
|
// failure
|
|
failDials := make(map[ma.Multiaddr]dialState)
|
|
// recvCh is used to receive dial notifications for dials that will fail
|
|
recvCh := make(chan struct{}, 100)
|
|
// allDials tracks all pending dials
|
|
allDials := make(map[ma.Multiaddr]dialState)
|
|
// addrs are the peer addresses the swarm will use for dialing
|
|
addrs := make([]ma.Multiaddr, 0)
|
|
// create pending dials
|
|
// we add success cases as a listen address on swarm
|
|
// failed cases are created using makeTCPListener
|
|
for _, inp := range tc.input {
|
|
var failCh chan struct{}
|
|
if inp.success {
|
|
// add the address as a listen address if this dial should succeed
|
|
err := s2.AddListenAddr(inp.addr)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to listen on addr: %s: err: %w", inp.addr, err)
|
|
}
|
|
} else {
|
|
// make a listener which will fail on sending a message to ch
|
|
l, ch := makeTCPListener(t, inp.addr, recvCh)
|
|
failCh = ch
|
|
f := func() {
|
|
err := l.Close()
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
defer f()
|
|
}
|
|
addrs = append(addrs, inp.addr)
|
|
// add to pending dials
|
|
allDials[inp.addr] = dialState{
|
|
ch: failCh,
|
|
addr: inp.addr,
|
|
delay: inp.delay,
|
|
success: inp.success,
|
|
failAfter: inp.failAfter,
|
|
}
|
|
}
|
|
// setup the peers addresses
|
|
s1.Peerstore().AddAddrs(s2.LocalPeer(), addrs, peerstore.PermanentAddrTTL)
|
|
|
|
// create worker
|
|
reqch := make(chan dialRequest)
|
|
resch := make(chan dialResponse)
|
|
cl := newMockClock()
|
|
st := cl.Now()
|
|
worker1 := newDialWorker(s1, s2.LocalPeer(), reqch, cl)
|
|
go worker1.loop()
|
|
defer worker1.wg.Wait()
|
|
defer close(reqch)
|
|
|
|
// trigger the request
|
|
reqch <- dialRequest{ctx: context.Background(), resch: resch}
|
|
|
|
connected := false
|
|
|
|
// Advance the clock by 10 ms every iteration
|
|
// At every iteration:
|
|
// Check if any dial should fail. if it should, trigger the failure by sending a message on the
|
|
// listener failCh
|
|
// If there are no dials in flight check the most urgent dials have been triggered
|
|
// If there are dials in flight check that the relevant dials have been triggered
|
|
// Before next iteration ensure that no unexpected dials are received
|
|
loop:
|
|
for {
|
|
// fail any dials that should fail at this instant
|
|
for a, p := range failDials {
|
|
if p.failAt.Before(cl.Now()) || p.failAt == cl.Now() {
|
|
p.ch <- struct{}{}
|
|
delete(failDials, a)
|
|
}
|
|
}
|
|
// if there are no pending dials, next dial should have been triggered
|
|
trigger := len(failDials) == 0
|
|
|
|
// mi is the minimum delay of pending dials
|
|
// if trigger is true, all dials with miDelay should have been triggered
|
|
mi := time.Duration(math.MaxInt64)
|
|
for _, ds := range allDials {
|
|
if ds.delay < mi {
|
|
mi = ds.delay
|
|
}
|
|
}
|
|
for a, ds := range allDials {
|
|
if (trigger && mi == ds.delay) ||
|
|
cl.Now().After(st.Add(ds.delay)) ||
|
|
cl.Now() == st.Add(ds.delay) {
|
|
if ds.success {
|
|
// check for success and exit
|
|
select {
|
|
case r := <-resch:
|
|
if r.conn == nil {
|
|
return errors.New("expected connection to succeed")
|
|
}
|
|
// High timeout here is okay. We will exit whenever the other branch
|
|
// is triggered
|
|
case <-time.After(10 * time.Second):
|
|
return errors.New("expected to receive a response")
|
|
}
|
|
connected = true
|
|
break loop
|
|
} else {
|
|
// ensure that a failing dial attempt happened but didn't succeed
|
|
select {
|
|
case <-recvCh:
|
|
case <-resch:
|
|
return errors.New("didn't expect a response")
|
|
// High timeout here is okay. We will exit whenever the other branch
|
|
// is triggered
|
|
case <-time.After(10 * time.Second):
|
|
return errors.New("didn't receive a dial attempt notification")
|
|
}
|
|
failDials[a] = dialState{
|
|
ch: ds.ch,
|
|
failAt: cl.Now().Add(ds.failAfter),
|
|
addr: a,
|
|
delay: ds.delay,
|
|
}
|
|
}
|
|
delete(allDials, a)
|
|
}
|
|
}
|
|
// check for unexpected dials
|
|
select {
|
|
case <-recvCh:
|
|
return errors.New("no dial should have succeeded at this instant")
|
|
default:
|
|
}
|
|
|
|
// advance the clock
|
|
cl.AdvanceBy(10 * time.Millisecond)
|
|
// nothing more to do. exit
|
|
if len(failDials) == 0 && len(allDials) == 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
if connected {
|
|
// ensure we don't receive any extra connections
|
|
select {
|
|
case <-recvCh:
|
|
return errors.New("didn't expect a dial attempt")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
} else {
|
|
// ensure that we do receive the final error response
|
|
select {
|
|
case r := <-resch:
|
|
require.Error(t, r.err)
|
|
case <-time.After(100 * time.Millisecond):
|
|
return errors.New("expected to receive response")
|
|
}
|
|
}
|
|
// check if this test didn't take too much time
|
|
if cl.Now().Sub(st) > tc.maxDuration {
|
|
return fmt.Errorf("expected test to finish early: expected %d, took: %d", tc.maxDuration, cl.Now().Sub(st))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// makeRanker takes a slice of timedDial objects and returns a DialRanker
|
|
// which will trigger dials to addresses at the specified delays in the timedDials
|
|
func makeRanker(tc []timedDial) network.DialRanker {
|
|
return func(addrs []ma.Multiaddr) []network.AddrDelay {
|
|
res := make([]network.AddrDelay, len(tc))
|
|
for i := 0; i < len(tc); i++ {
|
|
res[i] = network.AddrDelay{Addr: tc[i].addr, Delay: tc[i].delay}
|
|
}
|
|
return res
|
|
}
|
|
}
|
|
|
|
// TestCheckDialWorkerLoopScheduling will check the checker
|
|
func TestCheckDialWorkerLoopScheduling(t *testing.T) {
|
|
addrs := make([]ma.Multiaddr, 0)
|
|
for i := 0; i < 10; i++ {
|
|
for {
|
|
p := 20000 + i
|
|
addrs = append(addrs, ma.StringCast(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", p)))
|
|
break
|
|
}
|
|
}
|
|
|
|
tc := schedulingTestCase{
|
|
input: []timedDial{
|
|
{
|
|
addr: addrs[1],
|
|
delay: 0,
|
|
success: true,
|
|
},
|
|
{
|
|
addr: addrs[0],
|
|
delay: 100 * time.Millisecond,
|
|
success: false,
|
|
failAfter: 50 * time.Millisecond,
|
|
},
|
|
},
|
|
maxDuration: 20 * time.Millisecond,
|
|
}
|
|
s1 := makeSwarmWithNoListenAddrs(t)
|
|
s2 := makeSwarmWithNoListenAddrs(t)
|
|
// valid ranking logic, so it shouldn't error
|
|
s1.dialRanker = makeRanker(tc.input)
|
|
err := checkDialWorkerLoopScheduling(t, s1, s2, tc)
|
|
require.NoError(t, err)
|
|
// close swarms to remove address binding
|
|
s1.Close()
|
|
s2.Close()
|
|
|
|
s3 := makeSwarmWithNoListenAddrs(t)
|
|
defer s3.Close()
|
|
s4 := makeSwarmWithNoListenAddrs(t)
|
|
defer s4.Close()
|
|
// invalid ranking logic to trigger an error
|
|
s3.dialRanker = NoDelayDialRanker
|
|
err = checkDialWorkerLoopScheduling(t, s3, s4, tc)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestDialWorkerLoopRanking(t *testing.T) {
|
|
addrs := make([]ma.Multiaddr, 0)
|
|
for i := 0; i < 10; i++ {
|
|
for {
|
|
p := 20000 + i
|
|
addrs = append(addrs, ma.StringCast(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", p)))
|
|
break
|
|
}
|
|
}
|
|
|
|
testcases := []schedulingTestCase{
|
|
{
|
|
name: "first success",
|
|
input: []timedDial{
|
|
{
|
|
addr: addrs[1],
|
|
delay: 0,
|
|
success: true,
|
|
},
|
|
{
|
|
addr: addrs[0],
|
|
delay: 100 * time.Millisecond,
|
|
success: false,
|
|
failAfter: 50 * time.Millisecond,
|
|
},
|
|
},
|
|
maxDuration: 20 * time.Millisecond,
|
|
},
|
|
{
|
|
name: "delayed dials",
|
|
input: []timedDial{
|
|
{
|
|
addr: addrs[0],
|
|
delay: 0,
|
|
success: false,
|
|
failAfter: 200 * time.Millisecond,
|
|
},
|
|
{
|
|
addr: addrs[1],
|
|
delay: 100 * time.Millisecond,
|
|
success: false,
|
|
failAfter: 100 * time.Millisecond,
|
|
},
|
|
{
|
|
addr: addrs[2],
|
|
delay: 300 * time.Millisecond,
|
|
success: false,
|
|
failAfter: 100 * time.Millisecond,
|
|
},
|
|
{
|
|
addr: addrs[3],
|
|
delay: 2 * time.Second,
|
|
success: true,
|
|
},
|
|
{
|
|
addr: addrs[4],
|
|
delay: 2*time.Second + 1*time.Millisecond,
|
|
success: false, // this call will never happened
|
|
failAfter: 100 * time.Millisecond,
|
|
},
|
|
},
|
|
maxDuration: 310 * time.Millisecond,
|
|
},
|
|
{
|
|
name: "failed dials",
|
|
input: []timedDial{
|
|
{
|
|
addr: addrs[0],
|
|
delay: 0,
|
|
success: false,
|
|
failAfter: 105 * time.Millisecond,
|
|
},
|
|
{
|
|
addr: addrs[1],
|
|
delay: 100 * time.Millisecond,
|
|
success: false,
|
|
failAfter: 20 * time.Millisecond,
|
|
},
|
|
},
|
|
maxDuration: 200 * time.Millisecond,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testcases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
s1 := makeSwarmWithNoListenAddrs(t)
|
|
defer s1.Close()
|
|
s2 := makeSwarmWithNoListenAddrs(t)
|
|
defer s2.Close()
|
|
// setup the ranker to trigger dials according to the test case
|
|
s1.dialRanker = makeRanker(tc.input)
|
|
err := checkDialWorkerLoopScheduling(t, s1, s2, tc)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDialWorkerLoopSchedulingProperty(t *testing.T) {
|
|
f := func(tc schedulingTestCase) bool {
|
|
s1 := makeSwarmWithNoListenAddrs(t)
|
|
defer s1.Close()
|
|
// ignore limiter delays just check scheduling
|
|
s1.limiter.perPeerLimit = 10000
|
|
s2 := makeSwarmWithNoListenAddrs(t)
|
|
defer s2.Close()
|
|
// setup the ranker to trigger dials according to the test case
|
|
s1.dialRanker = makeRanker(tc.input)
|
|
err := checkDialWorkerLoopScheduling(t, s1, s2, tc)
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
return err == nil
|
|
}
|
|
|
|
if err := quick.Check(f, &quick.Config{MaxCount: 50}); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestDialWorkerLoopQuicOverTCP(t *testing.T) {
|
|
tc := schedulingTestCase{
|
|
input: []timedDial{
|
|
{
|
|
addr: ma.StringCast("/ip4/127.0.0.1/udp/20000/quic"),
|
|
delay: 0,
|
|
success: true,
|
|
},
|
|
{
|
|
addr: ma.StringCast("/ip4/127.0.0.1/tcp/20000"),
|
|
delay: 30 * time.Millisecond,
|
|
success: true,
|
|
},
|
|
},
|
|
maxDuration: 20 * time.Millisecond,
|
|
}
|
|
s1 := makeSwarmWithNoListenAddrs(t)
|
|
defer s1.Close()
|
|
|
|
s2 := makeSwarmWithNoListenAddrs(t)
|
|
defer s2.Close()
|
|
|
|
// we use the default ranker here
|
|
|
|
err := checkDialWorkerLoopScheduling(t, s1, s2, tc)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestDialWorkerLoopHolePunching(t *testing.T) {
|
|
s1 := makeSwarmWithNoListenAddrs(t)
|
|
defer s1.Close()
|
|
|
|
s2 := makeSwarmWithNoListenAddrs(t)
|
|
defer s2.Close()
|
|
|
|
// t1 will accept and keep the other end waiting
|
|
t1 := ma.StringCast("/ip4/127.0.0.1/tcp/10000")
|
|
recvCh := make(chan struct{})
|
|
list, ch := makeTCPListener(t, t1, recvCh) // ignore ch because we want to hang forever
|
|
defer list.Close()
|
|
defer func() { ch <- struct{}{} }() // close listener
|
|
|
|
// t2 will succeed
|
|
t2 := ma.StringCast("/ip4/127.0.0.1/tcp/10001")
|
|
|
|
err := s2.AddListenAddr(t2)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
s1.dialRanker = func(addrs []ma.Multiaddr) (res []network.AddrDelay) {
|
|
res = make([]network.AddrDelay, len(addrs))
|
|
for i := 0; i < len(addrs); i++ {
|
|
delay := 10 * time.Second
|
|
if addrs[i].Equal(t1) {
|
|
//fire t1 immediately
|
|
delay = 0
|
|
} else if addrs[i].Equal(t2) {
|
|
// delay t2 by 100ms
|
|
// without holepunch this call will not happen
|
|
delay = 100 * time.Millisecond
|
|
}
|
|
res[i] = network.AddrDelay{Addr: addrs[i], Delay: delay}
|
|
}
|
|
return
|
|
}
|
|
s1.Peerstore().AddAddrs(s2.LocalPeer(), []ma.Multiaddr{t1, t2}, peerstore.PermanentAddrTTL)
|
|
|
|
reqch := make(chan dialRequest)
|
|
resch := make(chan dialResponse, 2)
|
|
|
|
cl := newMockClock()
|
|
worker := newDialWorker(s1, s2.LocalPeer(), reqch, cl)
|
|
go worker.loop()
|
|
defer worker.wg.Wait()
|
|
defer close(reqch)
|
|
|
|
reqch <- dialRequest{ctx: context.Background(), resch: resch}
|
|
<-recvCh // received connection on t1
|
|
|
|
select {
|
|
case <-resch:
|
|
t.Errorf("didn't expect connection to succeed")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
|
|
hpCtx := network.WithSimultaneousConnect(context.Background(), true, "testing")
|
|
// with holepunch request, t2 will be dialed immediately
|
|
reqch <- dialRequest{ctx: hpCtx, resch: resch}
|
|
select {
|
|
case r := <-resch:
|
|
require.NoError(t, r.err)
|
|
case <-time.After(5 * time.Second):
|
|
t.Errorf("expected conn to succeed")
|
|
}
|
|
|
|
select {
|
|
case r := <-resch:
|
|
require.NoError(t, r.err)
|
|
case <-time.After(5 * time.Second):
|
|
t.Errorf("expected conn to succeed")
|
|
}
|
|
}
|
|
|
|
func TestDialWorkerLoopAddrDedup(t *testing.T) {
|
|
s1 := makeSwarm(t)
|
|
s2 := makeSwarm(t)
|
|
defer s1.Close()
|
|
defer s2.Close()
|
|
t1 := ma.StringCast(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", 10000))
|
|
t2 := ma.StringCast(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", 10000))
|
|
|
|
// acceptAndClose accepts a connection and closes it
|
|
acceptAndClose := func(a ma.Multiaddr, ch chan struct{}, closech chan struct{}) {
|
|
list, err := manet.Listen(a)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
go func() {
|
|
ch <- struct{}{}
|
|
for {
|
|
conn, err := list.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
ch <- struct{}{}
|
|
conn.Close()
|
|
}
|
|
}()
|
|
<-closech
|
|
list.Close()
|
|
}
|
|
ch := make(chan struct{}, 1)
|
|
closeCh := make(chan struct{})
|
|
go acceptAndClose(t1, ch, closeCh)
|
|
defer close(closeCh)
|
|
<-ch // the routine has started listening on addr
|
|
|
|
s1.Peerstore().AddAddrs(s2.LocalPeer(), []ma.Multiaddr{t1}, peerstore.PermanentAddrTTL)
|
|
|
|
reqch := make(chan dialRequest)
|
|
resch := make(chan dialResponse, 2)
|
|
|
|
worker := newDialWorker(s1, s2.LocalPeer(), reqch, nil)
|
|
go worker.loop()
|
|
defer worker.wg.Wait()
|
|
defer close(reqch)
|
|
|
|
reqch <- dialRequest{ctx: context.Background(), resch: resch}
|
|
<-ch
|
|
<-resch
|
|
// Need to clear backoff otherwise the dial attempt would not be made
|
|
s1.Backoff().Clear(s2.LocalPeer())
|
|
|
|
s1.Peerstore().ClearAddrs(s2.LocalPeer())
|
|
s1.Peerstore().AddAddrs(s2.LocalPeer(), []ma.Multiaddr{t2}, peerstore.PermanentAddrTTL)
|
|
|
|
reqch <- dialRequest{ctx: context.Background(), resch: resch}
|
|
select {
|
|
case r := <-resch:
|
|
require.Error(t, r.err)
|
|
case <-ch:
|
|
t.Errorf("didn't expect a connection attempt")
|
|
case <-time.After(5 * time.Second):
|
|
t.Errorf("expected a fail response")
|
|
}
|
|
}
|