credit_test.go raw

   1  package tunnel
   2  
   3  import (
   4  	"errors"
   5  	"strings"
   6  	"testing"
   7  	"time"
   8  
   9  	"git.smesh.lol/nostrino/pkg/crypto/secp256k1"
  10  )
  11  
  12  // bareSession builds a session with no relay behind it: enough to drive the flow-control
  13  // state machine directly. Sending an envelope fails with "no relay", which is why the tests
  14  // below ignore the error from a path that only sends an ack after it has already updated the
  15  // state under test.
  16  func bareSession(t *testing.T, window int) *session {
  17  	t.Helper()
  18  
  19  	key := keyWithByte(9)
  20  	peerKey := keyWithByte(11)
  21  	localPub, ok := secp256k1.PubKey(key)
  22  
  23  	if !ok {
  24  		t.Fatal("local key is not on the curve")
  25  	}
  26  
  27  	peerPub, ok := secp256k1.PubKey(peerKey)
  28  
  29  	if !ok {
  30  		t.Fatal("peer key is not on the curve")
  31  	}
  32  
  33  	s, err := newSession(sideClient, key, localPub, peerPub, "00", DialConfig{
  34  		Window:      window,
  35  		MTU:         1024,
  36  		Heartbeat:   time.Hour,
  37  		IdleTimeout: time.Hour,
  38  		Chain:       "regtest",
  39  	})
  40  
  41  	if err != nil {
  42  		t.Fatalf("newSession: %v", err)
  43  	}
  44  
  45  	return s
  46  }
  47  
  48  func dataFrame(seq uint32, body string) *envelope {
  49  	return &envelope{V: protocolVersion, T: frameData, Seq: seq, Data: []byte(body)}
  50  }
  51  
  52  // TestCreditCountsFramesHeldOutOfOrder: a frame parked in recvBuf has arrived and occupies
  53  // application room, so it must cost credit even though readQueue is empty. The old credit()
  54  // counted only readQueue, so a receiver holding a full reorder buffer advertised the whole
  55  // window again and invited a second window into the pipe.
  56  func TestCreditCountsFramesHeldOutOfOrder(t *testing.T) {
  57  	const window = 4
  58  
  59  	s := bareSession(t, window)
  60  
  61  	if got := s.credit(); got != window {
  62  		t.Fatalf("credit at rest = %d, want %d", got, window)
  63  	}
  64  
  65  	// Frame 2 arrives while 1 is missing: it is parked.
  66  	if err := s.handleData(dataFrame(2, "b")); err != nil {
  67  		t.Fatalf("buffering one ahead: %v", err)
  68  	}
  69  
  70  	if got := s.credit(); got != window-1 {
  71  		t.Fatalf("credit with one frame in recvBuf = %d, want %d", got, window-1)
  72  	}
  73  
  74  	// Filling the gap drains both frames into readQueue. They are both held now — the
  75  	// buffered one was already counted, and the in-order one is counted as it arrives — so
  76  	// credit must not rise merely because the frames moved.
  77  	_ = s.handleData(dataFrame(1, "a")) // no relay, so the ack cannot be sent
  78  
  79  	if got := s.credit(); got != window-2 {
  80  		t.Fatalf("credit with two frames in readQueue = %d, want %d", got, window-2)
  81  	}
  82  
  83  	buf := make([]byte, 8)
  84  	for i := 0; i < 2; i++ {
  85  		n, err := s.read(buf)
  86  
  87  		if err != nil {
  88  			t.Fatalf("read %d: %v", i, err)
  89  		}
  90  
  91  		if n != 1 {
  92  			t.Fatalf("read %d returned %d bytes, want 1", i, n)
  93  		}
  94  
  95  		if got, want := s.credit(), window-uint32(2-(i+1)); got != want {
  96  			t.Fatalf("credit after %d frame(s) read = %d, want %d", i+1, got, want)
  97  		}
  98  	}
  99  
 100  	if got := s.credit(); got != window {
 101  		t.Fatalf("credit after the application read everything = %d, want %d", got, window)
 102  	}
 103  }
 104  
 105  // TestAckCannotGrantMoreThanTheUnackedWindow: the peer's Win describes its application room,
 106  // but this sender's unacknowledged frames still occupy it. Credit is the smaller number, and
 107  // with no credit a send waits instead of overrunning the peer.
 108  func TestAckCannotGrantMoreThanTheUnackedWindow(t *testing.T) {
 109  	const window = 4
 110  
 111  	s := bareSession(t, window)
 112  
 113  	// Four frames sent, the peer has acknowledged three, frame 4 is still in the pipe.
 114  	s.stateMu.Lock()
 115  	s.sendSeq = 4
 116  	s.inflight = 1
 117  	s.sendCredit = window
 118  	s.stateMu.Unlock()
 119  
 120  	s.handleAck(&envelope{V: protocolVersion, T: frameAck, Ack: 3, Win: window})
 121  
 122  	if s.sendCredit != window-1 {
 123  		t.Fatalf("credit after ack 3 with one frame in flight = %d, want %d", s.sendCredit, window-1)
 124  	}
 125  
 126  	// A peer advertising the whole window must not raise credit above the sender's own
 127  	// unacknowledged bound.
 128  	s.stateMu.Lock()
 129  	s.inflight = window
 130  	s.sendCredit = window
 131  	s.stateMu.Unlock()
 132  
 133  	s.handleAck(&envelope{V: protocolVersion, T: frameAck, Ack: 4, Win: window})
 134  
 135  	if s.sendCredit != 0 {
 136  		t.Fatalf("credit with %d frames in flight = %d, want 0", window, s.sendCredit)
 137  	}
 138  
 139  	// A fifth send blocks rather than overrunning the window.
 140  	done := make(chan struct{})
 141  
 142  	go func() {
 143  		_ = s.acquireCredit()
 144  		close(done)
 145  	}()
 146  
 147  	select {
 148  	case <-done:
 149  		t.Fatal("a send took credit when the window was full")
 150  	case <-time.After(50 * time.Millisecond):
 151  	}
 152  
 153  	// The ack that retires a frame releases exactly one frame of credit.
 154  	s.stateMu.Lock()
 155  	s.inflight = window - 1
 156  	s.stateMu.Unlock()
 157  
 158  	s.handleAck(&envelope{V: protocolVersion, T: frameAck, Ack: 4, Win: window})
 159  
 160  	select {
 161  	case <-done:
 162  	case <-time.After(2 * time.Second):
 163  		t.Fatal("credit did not wake a waiting send")
 164  	}
 165  }
 166  
 167  // TestAFrameAWindowAheadIsRejected: with correct credit a sender has at most window frames
 168  // unacknowledged, so the furthest a frame can be ahead of recvNext is window-1. The old bound
 169  // tolerated window*2 and failed at 2*window+1 — the observed "65 ahead of 393" at window 32.
 170  func TestAFrameAWindowAheadIsRejected(t *testing.T) {
 171  	const window = 32
 172  
 173  	// The last displacement correct flow control can produce is accepted.
 174  	ok := bareSession(t, window)
 175  	ok.stateMu.Lock()
 176  	ok.recvNext = 393
 177  	ok.stateMu.Unlock()
 178  
 179  	if err := ok.handleData(dataFrame(393+window-1, "x")); err != nil {
 180  		t.Fatalf("a frame %d ahead (the last legal one): %v", window-1, err)
 181  	}
 182  
 183  	// A whole window ahead is the bound.
 184  	for _, ahead := range []uint32{window, 65} {
 185  		s := bareSession(t, window)
 186  		s.stateMu.Lock()
 187  		s.recvNext = 393
 188  		s.stateMu.Unlock()
 189  
 190  		err := s.handleData(dataFrame(393+ahead, "x"))
 191  
 192  		if !errors.Is(err, ErrFrameAhead) {
 193  			t.Fatalf("a frame %d ahead: %v, want ErrFrameAhead", ahead, err)
 194  		}
 195  
 196  		if !strings.Contains(err.Error(), "window is 32") {
 197  			t.Fatalf("the error does not name the window: %v", err)
 198  		}
 199  	}
 200  }
 201