From b4cee00af2fc10acaed23fdbb41e087c7927c23a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 30 Sep 2026 14:46:38 +0200 Subject: [PATCH] Ack Cut 3: a server's new generation does not inherit acknowledged bytes A server accepting a fresh Connect can send before its Accept is acknowledged, so no ack of the new generation prunes the acknowledged sizes; only `end_session` clearing them keeps the ended generation's bytes from counting against the new window. Rust, TypeScript, Python and C# each pin it: 4 MiB acknowledged above a lost packet, a fresh Connect, and the server's first 1 MiB send of the new generation goes out. Co-Authored-By: Claude Opus 5.5 --- packages/cultnet-py/tests/test_cultnet.py | 19 +++++++++++++ packages/cultnet-rs/tests/rudp_flow_window.rs | 28 +++++++++++++++++++ packages/cultnet-ts/test/cultnet.test.ts | 20 +++++++++++++ .../NetworkingTests.cs | 24 ++++++++++++++++ 4 files changed, 91 insertions(+) diff --git a/packages/cultnet-py/tests/test_cultnet.py b/packages/cultnet-py/tests/test_cultnet.py index df6e2340..4c740ac9 100644 --- a/packages/cultnet-py/tests/test_cultnet.py +++ b/packages/cultnet-py/tests/test_cultnet.py @@ -1017,6 +1017,25 @@ class CultNetTests(unittest.TestCase): promoted = sender.receive(self._flow_ack(h.sequence), 2).ready_to_send self.assertEqual([p.sequence for p in promoted], [g.sequence + 6]) + def test_cultnet_rudp_a_new_generation_on_the_server_does_not_inherit_acknowledged_bytes(self) -> None: + # A server that accepts a fresh Connect may send before its Accept is acknowledged, so no acknowledgement of + # the new generation has moved the floor yet. Bytes the ended generation had acknowledged above its own lost + # packet belong to no receiver's hold any more and must not count. + server = self._flow_session(500) + accept = server.accept_connect(self._flow_session(1).create_connect(0), 0) + server.receive(self._flow_ack(accept.sequence), 0) + g = self._flow_send(server, 1)[0] + for offset in range(1, 5): + self.assertEqual([p.sequence for p in self._flow_send(server, self.MIB)], [g.sequence + offset]) + server.receive(self._flow_ack(g.sequence + offset), 1) + self.assertEqual(len(self._flow_send(server, self.MIB)), 0) + + next_accept = server.accept_connect(self._flow_session(200_000).create_connect(2), 2) + self.assertEqual(next_accept.packet_type, CultNetRudpPacketType.ACCEPT) + # The Accept is unacknowledged and the only thing pending: 1 MiB fits above it. + sent = server.send("state", bytes(self.MIB), CultNetRudpSendOptions(reliable=True)) + self.assertEqual(sent.sequence, next_accept.sequence + 1) + def test_cultnet_rudp_socket_transport_handshakes_and_carries_reliable_ordered_schema_frames(self) -> None: server_socket = bind_udp_socket() client_socket = bind_udp_socket() diff --git a/packages/cultnet-rs/tests/rudp_flow_window.rs b/packages/cultnet-rs/tests/rudp_flow_window.rs index 58065970..9f10eb50 100644 --- a/packages/cultnet-rs/tests/rudp_flow_window.rs +++ b/packages/cultnet-rs/tests/rudp_flow_window.rs @@ -280,3 +280,31 @@ fn a_direct_send_is_not_refused_for_bytes_whose_lowest_packet_has_expired() -> R assert_eq!(sender.outstanding_reliable_packet_count(), 1); Ok(()) } + +/// A server that accepts a fresh Connect may send before its Accept is +/// acknowledged, so no acknowledgement of the new generation has moved the +/// floor yet. Bytes the ended generation had acknowledged above its own lost +/// packet belong to no receiver's hold any more and must not count. +#[test] +fn a_new_generation_on_the_server_does_not_inherit_acknowledged_bytes() -> Result<()> { + let mut server = session(500); + let mut first_client = session(1); + let connect = first_client.create_connect(0, Vec::new())?; + let accept = server.accept_connect(&connect, 0, Vec::new())?; + server.receive(&ack_for(accept.sequence), 0)?; + let g = send(&mut server, 1)?.remove(0); + for offset in 1..=4u32 { + assert_eq!(sequences(&send(&mut server, MIB)?), vec![g.sequence + offset]); + server.receive(&ack_for(g.sequence + offset), 1)?; + } + assert!(send(&mut server, MIB)?.is_empty()); + + let mut second_client = session(200_000); + let connect = second_client.create_connect(2, Vec::new())?; + let accept = server.accept_connect(&connect, 2, Vec::new())?; + assert_eq!(accept.packet_type, CultNetRudpPacketType::Accept); + // The Accept is unacknowledged and the only thing pending: 1 MiB fits above it. + let sent = server.send("state", vec![7; MIB], reliable())?; + assert_eq!(sent.sequence, accept.sequence + 1); + Ok(()) +} diff --git a/packages/cultnet-ts/test/cultnet.test.ts b/packages/cultnet-ts/test/cultnet.test.ts index bf3f64bb..e6b62bdb 100644 --- a/packages/cultnet-ts/test/cultnet.test.ts +++ b/packages/cultnet-ts/test/cultnet.test.ts @@ -1558,6 +1558,26 @@ test("acknowledged bytes count against the lowest unacked sequence as it advance assert.deepEqual(sequencesOf(sender.receive(flowAck(h.sequence), 2).readyToSend ?? []), [g.sequence + 6]); }); +// A server that accepts a fresh Connect may send before its Accept is acknowledged, so no acknowledgement of the new +// generation has moved the floor yet. Bytes the ended generation had acknowledged above its own lost packet belong to +// no receiver's hold any more and must not count. +test("a new generation on the server does not inherit acknowledged bytes", () => { + const server = flowSession(500); + const accept = server.acceptConnect(flowSession(1).createConnect(0), 0); + server.receive(flowAck(accept.sequence), 0); + const g = flowSend(server, 1)[0]!; + for (let offset = 1; offset <= 4; offset++) { + assert.deepEqual(sequencesOf(flowSend(server, MIB)), [g.sequence + offset]); + server.receive(flowAck(g.sequence + offset), 1); + } + assert.equal(flowSend(server, MIB).length, 0); + + const nextAccept = server.acceptConnect(flowSession(200_000).createConnect(2), 2); + assert.equal(nextAccept.packetType, "accept"); + // The Accept is unacknowledged and the only thing pending: 1 MiB fits above it. + assert.equal(server.send("state", new Uint8Array(MIB), { reliable: true }).sequence, nextAccept.sequence + 1); +}); + test("CultNet contracts encode legacy bytes without Node Buffer authority", () => { const originalBuffer = globalThis.Buffer; try { diff --git a/tests/GameCult.Networking.Tests/NetworkingTests.cs b/tests/GameCult.Networking.Tests/NetworkingTests.cs index 56d0483b..5a1f570f 100644 --- a/tests/GameCult.Networking.Tests/NetworkingTests.cs +++ b/tests/GameCult.Networking.Tests/NetworkingTests.cs @@ -1307,6 +1307,30 @@ namespace GameCult.Networking.Tests Assert.That(promoted.Select(packet => packet.Sequence), Is.EqualTo(new[] { g.Sequence + 6 })); } + // A server that accepts a fresh Connect may send before its Accept is acknowledged, so no acknowledgement of + // the new generation has moved the floor yet. Bytes the ended generation had acknowledged above its own lost + // packet belong to no receiver's hold any more and must not count. + [Test] + public void RudpSession_ANewGenerationOnTheServerDoesNotInheritAcknowledgedBytes() + { + var server = FlowSession(500); + var accept = server.AcceptConnect(FlowSession(1).CreateConnect(0), 0); + server.Receive(FlowAck(accept.Sequence), 0); + var g = FlowSend(server, 1).Single(); + for (uint offset = 1; offset <= 4; offset++) + { + Assert.That(FlowSend(server, Mib).Select(packet => packet.Sequence), Is.EqualTo(new[] { g.Sequence + offset })); + server.Receive(FlowAck(g.Sequence + offset), 1); + } + Assert.That(FlowSend(server, Mib), Is.Empty); + + var nextAccept = server.AcceptConnect(FlowSession(200_000).CreateConnect(2), 2); + Assert.That(nextAccept.PacketType, Is.EqualTo(CultNetRudpPacketType.Accept)); + // The Accept is unacknowledged and the only thing pending: 1 MiB fits above it. + var sent = server.Send("state", new byte[Mib], new CultNetRudpSendOptions { Reliable = true }); + Assert.That(sent.Sequence, Is.EqualTo(nextAccept.Sequence + 1)); + } + [Test] public void RudpSocketTransport_HandshakesAndCarriesReliableOrderedSchemaFrames() {