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 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 14:46:38 +02:00
commit b4cee00af2
4 changed files with 91 additions and 0 deletions

View file

@ -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()

View file

@ -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(())
}

View file

@ -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 {

View file

@ -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()
{