package tunnel import ( "errors" "strings" "testing" "time" "git.smesh.lol/nostrino/pkg/crypto/secp256k1" ) // bareSession builds a session with no relay behind it: enough to drive the flow-control // state machine directly. Sending an envelope fails with "no relay", which is why the tests // below ignore the error from a path that only sends an ack after it has already updated the // state under test. func bareSession(t *testing.T, window int) *session { t.Helper() key := keyWithByte(9) peerKey := keyWithByte(11) localPub, ok := secp256k1.PubKey(key) if !ok { t.Fatal("local key is not on the curve") } peerPub, ok := secp256k1.PubKey(peerKey) if !ok { t.Fatal("peer key is not on the curve") } s, err := newSession(sideClient, key, localPub, peerPub, "00", DialConfig{ Window: window, MTU: 1024, Heartbeat: time.Hour, IdleTimeout: time.Hour, Chain: "regtest", }) if err != nil { t.Fatalf("newSession: %v", err) } return s } func dataFrame(seq uint32, body string) *envelope { return &envelope{V: protocolVersion, T: frameData, Seq: seq, Data: []byte(body)} } // TestCreditCountsFramesHeldOutOfOrder: a frame parked in recvBuf has arrived and occupies // application room, so it must cost credit even though readQueue is empty. The old credit() // counted only readQueue, so a receiver holding a full reorder buffer advertised the whole // window again and invited a second window into the pipe. func TestCreditCountsFramesHeldOutOfOrder(t *testing.T) { const window = 4 s := bareSession(t, window) if got := s.credit(); got != window { t.Fatalf("credit at rest = %d, want %d", got, window) } // Frame 2 arrives while 1 is missing: it is parked. if err := s.handleData(dataFrame(2, "b")); err != nil { t.Fatalf("buffering one ahead: %v", err) } if got := s.credit(); got != window-1 { t.Fatalf("credit with one frame in recvBuf = %d, want %d", got, window-1) } // Filling the gap drains both frames into readQueue. They are both held now — the // buffered one was already counted, and the in-order one is counted as it arrives — so // credit must not rise merely because the frames moved. _ = s.handleData(dataFrame(1, "a")) // no relay, so the ack cannot be sent if got := s.credit(); got != window-2 { t.Fatalf("credit with two frames in readQueue = %d, want %d", got, window-2) } buf := make([]byte, 8) for i := 0; i < 2; i++ { n, err := s.read(buf) if err != nil { t.Fatalf("read %d: %v", i, err) } if n != 1 { t.Fatalf("read %d returned %d bytes, want 1", i, n) } if got, want := s.credit(), window-uint32(2-(i+1)); got != want { t.Fatalf("credit after %d frame(s) read = %d, want %d", i+1, got, want) } } if got := s.credit(); got != window { t.Fatalf("credit after the application read everything = %d, want %d", got, window) } } // TestAckCannotGrantMoreThanTheUnackedWindow: the peer's Win describes its application room, // but this sender's unacknowledged frames still occupy it. Credit is the smaller number, and // with no credit a send waits instead of overrunning the peer. func TestAckCannotGrantMoreThanTheUnackedWindow(t *testing.T) { const window = 4 s := bareSession(t, window) // Four frames sent, the peer has acknowledged three, frame 4 is still in the pipe. s.stateMu.Lock() s.sendSeq = 4 s.inflight = 1 s.sendCredit = window s.stateMu.Unlock() s.handleAck(&envelope{V: protocolVersion, T: frameAck, Ack: 3, Win: window}) if s.sendCredit != window-1 { t.Fatalf("credit after ack 3 with one frame in flight = %d, want %d", s.sendCredit, window-1) } // A peer advertising the whole window must not raise credit above the sender's own // unacknowledged bound. s.stateMu.Lock() s.inflight = window s.sendCredit = window s.stateMu.Unlock() s.handleAck(&envelope{V: protocolVersion, T: frameAck, Ack: 4, Win: window}) if s.sendCredit != 0 { t.Fatalf("credit with %d frames in flight = %d, want 0", window, s.sendCredit) } // A fifth send blocks rather than overrunning the window. done := make(chan struct{}) go func() { _ = s.acquireCredit() close(done) }() select { case <-done: t.Fatal("a send took credit when the window was full") case <-time.After(50 * time.Millisecond): } // The ack that retires a frame releases exactly one frame of credit. s.stateMu.Lock() s.inflight = window - 1 s.stateMu.Unlock() s.handleAck(&envelope{V: protocolVersion, T: frameAck, Ack: 4, Win: window}) select { case <-done: case <-time.After(2 * time.Second): t.Fatal("credit did not wake a waiting send") } } // TestAFrameAWindowAheadIsRejected: with correct credit a sender has at most window frames // unacknowledged, so the furthest a frame can be ahead of recvNext is window-1. The old bound // tolerated window*2 and failed at 2*window+1 — the observed "65 ahead of 393" at window 32. func TestAFrameAWindowAheadIsRejected(t *testing.T) { const window = 32 // The last displacement correct flow control can produce is accepted. ok := bareSession(t, window) ok.stateMu.Lock() ok.recvNext = 393 ok.stateMu.Unlock() if err := ok.handleData(dataFrame(393+window-1, "x")); err != nil { t.Fatalf("a frame %d ahead (the last legal one): %v", window-1, err) } // A whole window ahead is the bound. for _, ahead := range []uint32{window, 65} { s := bareSession(t, window) s.stateMu.Lock() s.recvNext = 393 s.stateMu.Unlock() err := s.handleData(dataFrame(393+ahead, "x")) if !errors.Is(err, ErrFrameAhead) { t.Fatalf("a frame %d ahead: %v, want ErrFrameAhead", ahead, err) } if !strings.Contains(err.Error(), "window is 32") { t.Fatalf("the error does not name the window: %v", err) } } }