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