using System.Buffers; using System.Buffers.Binary; using System.Net; using AcDream.Core.Net.Cryptography; using AcDream.Core.Net.Messages; using AcDream.Core.Net.Packets; using AcDream.Core.Net.Transport; namespace AcDream.Core.Net.Tests.Transport; /// /// Campaign N Slice N1 — the outbound sent-packet cache + resend on NAK. /// Unit tests pin the store/clock/sequence primitives; conformance tests /// grade the resend against the N0 ACE-behaviour double (the rebuilt-header /// resend must verify under with the ORIGINAL /// keystream word — campaign landmines #1/#2). /// public sealed class OutboundReliableTransportTests { private const uint ClientSeed = 0x11AA22BBu; private const uint ServerSeed = 0x33CC44DDu; private const uint ClientId = 0x1234u; private const ushort SessionIteration = 0x0007; private const ulong Cookie = 0xFEEDFACECAFEBABEUL; // ===================================================================== // SequenceMath — TimeStampUtils::lhs_newer @ 0x00543890 // ===================================================================== [Fact] public void SequenceMath_IsNewer_IsWrapSafe() { Assert.True(SequenceMath.IsNewer(2u, 1u)); Assert.False(SequenceMath.IsNewer(1u, 2u)); Assert.False(SequenceMath.IsNewer(7u, 7u)); // Across the 32-bit wrap: 1 is newer than 0xFFFFFFFF. Assert.True(SequenceMath.IsNewer(1u, uint.MaxValue)); Assert.False(SequenceMath.IsNewer(uint.MaxValue, 1u)); Assert.Equal(5u, SequenceMath.Max(5u, 3u)); Assert.Equal(5u, SequenceMath.Max(3u, 5u)); // Wrap-safe max: a small post-wrap value beats a huge pre-wrap one. Assert.Equal(5u, SequenceMath.Max(0xFFFFFFF6u, 5u)); } // ===================================================================== // TransportClock — ClientFlowQueue::IncrementLocalInterval @ 0x00547F10 // ===================================================================== [Fact] public void TransportClock_StartsAtOne_AdvancesEveryHalfSecond_AndWraps() { var virtualClock = new VirtualClock(); var clock = new TransportClock( virtualClock.GetTimestamp, virtualClock.Frequency); Assert.Equal((ushort)1, clock.IntervalId); // Under half a second: no advance. virtualClock.Advance(TimeSpan.FromSeconds(0.49)); clock.Update(); Assert.Equal((ushort)1, clock.IntervalId); // Crossing 0.5 s advances one interval. virtualClock.Advance(TimeSpan.FromSeconds(0.01)); clock.Update(); Assert.Equal((ushort)2, clock.IntervalId); // A long gap advances by the whole number of elapsed intervals, // preserving the fractional remainder. virtualClock.Advance(TimeSpan.FromSeconds(2.75)); clock.Update(); Assert.Equal((ushort)7, clock.IntervalId); virtualClock.Advance(TimeSpan.FromSeconds(0.25)); clock.Update(); Assert.Equal((ushort)8, clock.IntervalId); // Natural ushort wrap: 65531 more intervals take 8 → 3 (mod 65536). virtualClock.Advance(TimeSpan.FromSeconds(0.5 * 65531)); clock.Update(); Assert.Equal((ushort)3, clock.IntervalId); } // ===================================================================== // SentPacketStore — AddSentPacket @ 0x0054AB00 / Flush @ 0x0054ACD0 // ===================================================================== [Fact] public void SentPacketStore_FifoContainsAndStrictFlush() { var pool = new CountingPool(); using var store = new SentPacketStore(pool); store.Add(RentedEntry(pool, 2u), optionalLength: 0); store.Add(RentedEntry(pool, 3u), optionalLength: 0); store.Add(RentedEntry(pool, 4u), optionalLength: 0); Assert.Equal(3, store.Count); Assert.True(store.Contains(3u)); Assert.False(store.Contains(5u)); Assert.True(store.TryGet(2u, out SentPacketStore.CachedPacket got)); Assert.Equal(2u, got.Sequence); // STRICTLY older: the watermark entry itself survives // (SentPacketStore::Flush breaks at seqNum_ == watermark). store.FlushOlderThan(3u); Assert.Equal(2, store.Count); Assert.False(store.Contains(2u)); Assert.True(store.Contains(3u)); Assert.True(store.Contains(4u)); Assert.Equal(1, pool.Returned); store.FlushOlderThan(5u); Assert.Equal(0, store.Count); Assert.Equal(3, pool.Returned); Assert.Equal(pool.Rented, pool.Returned); } [Fact] public void SentPacketStore_FlushIsWrapSafe_AcrossTheSequenceWrap() { var pool = new CountingPool(); using var store = new SentPacketStore(pool); // Retail wraps 0xFFFFFFFF → 1 (never 0). store.Add(RentedEntry(pool, 0xFFFFFFFEu), optionalLength: 0); store.Add(RentedEntry(pool, 0xFFFFFFFFu), optionalLength: 0); store.Add(RentedEntry(pool, 1u), optionalLength: 0); store.Add(RentedEntry(pool, 2u), optionalLength: 0); // Watermark 1 (post-wrap): both pre-wrap entries are strictly // older; 1 and 2 survive. A raw `<` compare would flush nothing. store.FlushOlderThan(1u); Assert.Equal(2, store.Count); Assert.False(store.Contains(0xFFFFFFFEu)); Assert.False(store.Contains(0xFFFFFFFFu)); Assert.True(store.Contains(1u)); Assert.True(store.Contains(2u)); Assert.Equal(2, pool.Returned); store.FlushOlderThan(3u); Assert.Equal(0, store.Count); Assert.Equal(pool.Rented, pool.Returned); } [Fact] public void SentPacketStore_Dispose_ReturnsEveryRentedBuffer() { var pool = new CountingPool(); var store = new SentPacketStore(pool); store.Add(RentedEntry(pool, 2u), optionalLength: 0); store.Add(RentedEntry(pool, 3u), optionalLength: 0); store.Dispose(); Assert.Equal(pool.Rented, pool.Returned); } [Fact] public void SentPacketStore_Add_AssertsNoOptionalHeaders() { var pool = new CountingPool(); using var store = new SentPacketStore(pool); SentPacketStore.CachedPacket entry = RentedEntry(pool, 2u); Assert.Throws( () => store.Add(entry, optionalLength: 4)); pool.Return(entry.Buffer); // the failed Add never took ownership } // ===================================================================== // OutboundFlowQueue — resend header rebuild (landmines #1/#2/#3) // ===================================================================== [Fact] public void Resend_RebuildsHeaderOnly_FlagsTimeChecksum_BodyBitIdentical() { (OutboundFlowQueue queue, VirtualClock virtualClock, TransportClock clock, TransportStats stats, List sent) = CreateQueue(); queue.SendGameMessage(MakeMessage(0xA1), GameMessageGroup.UIQueue); byte[] original = Assert.Single(sent); PacketHeader originalHeader = PacketHeader.Unpack(original); Assert.Equal(2u, originalHeader.Sequence); Assert.Equal( PacketHeaderFlags.BlobFragments | PacketHeaderFlags.EncryptedChecksum, originalHeader.Flags); // N3 fold-in: fresh sends stamp the current interval id (the clock // starts at 1) — FlowQueue::TransmitNewPackets @ 0x00547A60. Assert.Equal((ushort)1, originalHeader.Time); // N5 fold-in (N4 review F3): fresh sends stamp the session iteration // through the same shared header build (0x00547A84/0x00547AA8), // completing the control-header rule across all three send shapes. Assert.Equal(SessionIteration, originalHeader.Iteration); // 1.2 s later (interval id 1 → 3) the server NAKs sequence 2. virtualClock.Advance(TimeSpan.FromSeconds(1.2)); clock.Update(); Nak(queue, 2u); sent.Clear(); queue.TransmitPendingResends(); byte[] resent = Assert.Single(sent); PacketHeader resentHeader = PacketHeader.Unpack(resent); // Flags become EXACTLY Retransmission|EncryptedChecksum|BlobFragments // (= 7 with fragments); Time advances to the current interval; // Sequence/Id/Iteration/DataSize stay verbatim. Assert.Equal( PacketHeaderFlags.Retransmission | PacketHeaderFlags.EncryptedChecksum | PacketHeaderFlags.BlobFragments, resentHeader.Flags); Assert.Equal((uint)7, (uint)resentHeader.Flags); Assert.Equal((ushort)3, resentHeader.Time); Assert.Equal(originalHeader.Sequence, resentHeader.Sequence); Assert.Equal(originalHeader.DataSize, resentHeader.DataSize); Assert.Equal(originalHeader.Id, resentHeader.Id); Assert.Equal(originalHeader.Iteration, resentHeader.Iteration); // Checksum = FRESH header hash + the stored sealed checksum, where // sealed = originalChecksum − originalHeaderHash (landmine #1). uint sealedChecksum = originalHeader.Checksum - originalHeader.CalculateHeaderHash32(); Assert.Equal( resentHeader.CalculateHeaderHash32() + sealedChecksum, resentHeader.Checksum); // Body bytes bit-identical. Assert.Equal( original.AsSpan(PacketHeader.Size).ToArray(), resent.AsSpan(PacketHeader.Size).ToArray()); Assert.Equal(1, stats.ResendsSent); Assert.Equal(1, stats.NakRequestsReceived); Assert.Equal(0, stats.UncachedNakIds); } [Fact] public void BuildResendHeader_WithoutFragments_FlagsAreExactlyThree() { // Fragmentless reliable packets do not exist on the N1 send path // (every reliable message rides a fragment), but the rebuild rule is // pinned for both shapes: 3 without fragments, 7 with. byte[] buffer = new byte[PacketHeader.Size]; var header = new PacketHeader { Sequence = 9u, Flags = PacketHeaderFlags.EncryptedChecksum, Id = 0x1234, DataSize = 0, }; header.Pack(buffer); var cached = new SentPacketStore.CachedPacket( 9u, buffer, bodyLength: 0, sealedChecksum: 0xDEADBEEFu, isaacKey: 0u, hasFragments: false); PacketHeader rebuilt = OutboundFlowQueue.BuildResendHeader(in cached, intervalId: 42); Assert.Equal( PacketHeaderFlags.Retransmission | PacketHeaderFlags.EncryptedChecksum, rebuilt.Flags); Assert.Equal((uint)3, (uint)rebuilt.Flags); Assert.Equal((ushort)42, rebuilt.Time); Assert.Equal(9u, rebuilt.Sequence); Assert.Equal( rebuilt.CalculateHeaderHash32() + 0xDEADBEEFu, rebuilt.Checksum); } [Fact] public void Resend_ConsumesNoOutboundIsaacWord() { (OutboundFlowQueue queue, _, _, _, List sent) = CreateQueue(); IsaacRandom shadow = MakeIsaac(ClientSeed); uint w1 = shadow.Next(); uint w2 = shadow.Next(); uint w3 = shadow.Next(); queue.SendGameMessage(MakeMessage(0xA1), GameMessageGroup.UIQueue); // seq 2, w1 queue.SendGameMessage(MakeMessage(0xB2), GameMessageGroup.UIQueue); // seq 3, w2 Assert.Equal(w1, ExtractIsaacKey(sent[0])); Assert.Equal(w2, ExtractIsaacKey(sent[1])); // Resend of seq 2 reuses w1 — no keystream word drawn (landmine #2). Nak(queue, 2u); sent.Clear(); queue.TransmitPendingResends(); Assert.Equal(w1, ExtractIsaacKey(Assert.Single(sent))); // The wheel did not move: the next fresh packet takes w3. sent.Clear(); queue.SendGameMessage(MakeMessage(0xC3), GameMessageGroup.UIQueue); // seq 4, w3 Assert.Equal(w3, ExtractIsaacKey(Assert.Single(sent))); } [Fact] public void Nak_ForUncachedId_SendsNothing_AndCounts() { (OutboundFlowQueue queue, _, _, TransportStats stats, List sent) = CreateQueue(); queue.SendGameMessage(MakeMessage(0xA1), GameMessageGroup.UIQueue); // seq 2 sent.Clear(); // Id 40 was never sent: dropped silently + counted (TS-57 — retail // answers RejectRetransmit; ACE no-ops it and the standalone form // would trip the watermark hole). Nak(queue, 40u); queue.TransmitPendingResends(); Assert.Empty(sent); Assert.Equal(1, stats.UncachedNakIds); Assert.Equal(0, stats.ResendsSent); Assert.Equal(0, queue.PendingResendCount); } [Fact] public void NakFirstId_FoldsTheAckWatermark_AndSweepPrunesStrictlyBelow() { (OutboundFlowQueue queue, _, _, TransportStats stats, List sent) = CreateQueue(); queue.SendGameMessage(MakeMessage(0xA1), GameMessageGroup.UIQueue); // seq 2 queue.SendGameMessage(MakeMessage(0xB2), GameMessageGroup.UIQueue); // seq 3 queue.SendGameMessage(MakeMessage(0xC3), GameMessageGroup.UIQueue); // seq 4 Assert.Equal(3, queue.CacheDepth); sent.Clear(); // ids[0] = 4 doubles as the implicit cumulative ack // (RecipientData::ProcessNaks @ 0x00547010): everything strictly // below 4 prunes on the sweep; 4 itself is served. Nak(queue, 4u); Assert.Equal(4u, queue.AckWatermark); queue.TransmitPendingResends(); Assert.Equal(4u, PacketHeader.Unpack(Assert.Single(sent)).Sequence); Assert.Equal(1, queue.CacheDepth); Assert.True(stats.AcksConsumed >= 1); } /// /// N3 fold-in of the N1 review advisory: retail stamps /// CurLocalInterval_.intervalID_ into Header.Time on every /// FRESH packet (FlowQueue::TransmitNewPackets @ 0x00547A60, the /// header build at 0x00547A84), and a resend re-stamps the CURRENT /// interval id (possibly newer than the fresh-send stamp). ACE never /// reads inbound Header.Time, so this is wire-cosmetic against /// ACE — but it is retail's behavior. /// [Fact] public void FreshSend_StampsCurrentIntervalId_ResendRestampsNewer() { (OutboundFlowQueue queue, VirtualClock virtualClock, TransportClock clock, _, List sent) = CreateQueue(); // K interval ticks before the send: 2.5 s = 5 intervals, id 1 → 6. virtualClock.Advance(TimeSpan.FromSeconds(2.5)); clock.Update(); Assert.Equal((ushort)6, clock.IntervalId); queue.SendGameMessage(MakeMessage(0xA1), GameMessageGroup.UIQueue); PacketHeader freshHeader = PacketHeader.Unpack(Assert.Single(sent)); Assert.Equal((ushort)6, freshHeader.Time); // N5 fold-in (N4 review F3): the fresh send carries the session // iteration, and the resend below keeps it verbatim. Assert.Equal(SessionIteration, freshHeader.Iteration); // The interval advances again; the resend carries the CURRENT id, // newer than the fresh-send stamp. virtualClock.Advance(TimeSpan.FromSeconds(1.0)); clock.Update(); Assert.Equal((ushort)8, clock.IntervalId); Nak(queue, 2u); sent.Clear(); queue.TransmitPendingResends(); PacketHeader resentHeader = PacketHeader.Unpack(Assert.Single(sent)); Assert.Equal((ushort)8, resentHeader.Time); Assert.Equal(SessionIteration, resentHeader.Iteration); } [Fact] public void OnAckSequence_IsWrapSafeMax_AndNeverRegresses() { (OutboundFlowQueue queue, _, _, _, _) = CreateQueue(); queue.OnAckSequence(10u); Assert.Equal(10u, queue.AckWatermark); queue.OnAckSequence(3u); // stale ack must not roll the watermark back Assert.Equal(10u, queue.AckWatermark); // Across the wrap: walk the watermark up in half-window-safe steps // (like a live sequence stream does), then a small post-wrap value // is NEWER than the huge pre-wrap one — and the reverse is stale. (OutboundFlowQueue wrapQueue, _, _, _, _) = CreateQueue(); wrapQueue.OnAckSequence(0x60000000u); wrapQueue.OnAckSequence(0xC0000000u); wrapQueue.OnAckSequence(0xFFFFFFF6u); Assert.Equal(0xFFFFFFF6u, wrapQueue.AckWatermark); wrapQueue.OnAckSequence(5u); // newer across the wrap Assert.Equal(5u, wrapQueue.AckWatermark); wrapQueue.OnAckSequence(0xFFFFFFF6u); // now stale — must not regress Assert.Equal(5u, wrapQueue.AckWatermark); } // ===================================================================== // Conformance against the N0 ACE-behaviour double // ===================================================================== [Fact] public void RebuiltResend_VerifiesUnderAceCrypto_WithTheOriginalKey() { (AceSessionModel model, _) = CreateNegotiatedModel(); (OutboundFlowQueue queue, _, _, _, List sent) = CreateQueue(); // Four reliable packets, seq 2..5; seq 3 is "lost". queue.SendGameMessage(MakeMessage(2), GameMessageGroup.UIQueue); queue.SendGameMessage(MakeMessage(3), GameMessageGroup.UIQueue); queue.SendGameMessage(MakeMessage(4), GameMessageGroup.UIQueue); queue.SendGameMessage(MakeMessage(5), GameMessageGroup.UIQueue); model.Receive(sent[0]); // seq 2 in order model.Receive(sent[2]); // seq 4: buffered, gap of one — no NAK yet model.Receive(sent[3]); // seq 5: desired+2 ≤ arrived → NAK fires model.Update(); byte[] nak = Assert.Single( model.TakePendingDatagrams(), d => PacketHeader.Unpack(d).Flags == PacketHeaderFlags.RequestRetransmit); // Feed the genuine ACE NAK bytes through the same parse the session // uses, then sweep: exactly one rebuilt-header resend goes out. PacketCodec.PacketDecodeResult decodedNak = PacketCodec.TryDecode(nak, inboundIsaac: null); Assert.True(decodedNak.IsOk, decodedNak.Error.ToString()); uint[] ids = decodedNak.Packet!.Optional.RetransmitRequests.ToArray(); Assert.Equal(new uint[] { 3u }, ids); sent.Clear(); Nak(queue, ids); queue.TransmitPendingResends(); byte[] resent = Assert.Single(sent); Assert.Equal( PacketHeaderFlags.Retransmission | PacketHeaderFlags.EncryptedChecksum | PacketHeaderFlags.BlobFragments, PacketHeader.Unpack(resent).Flags); // ACE verifies the rebuilt form with the PARKED ORIGINAL key: the // 256-key window fully recovers, no orphan, ordering restored. model.Receive(resent); Assert.Equal( new byte[] { 2, 3, 4, 5 }, model.DispatchedMessages.Select(m => m[0]).ToArray()); Assert.Equal(5u, model.LastReceivedPacketSequence); Assert.Equal(0, model.CrcDropCount); Assert.Equal(0, model.DuplicateDropCount); Assert.Equal(256, model.Crypto.Headroom); Assert.Equal(0, model.Crypto.OrphanCount); // ids[0] = 3 folded as the implicit ack: seq 2 pruned on the sweep, // 3..5 still cached. Assert.Equal(3, queue.CacheDepth); } /// /// The #260 fix end-to-end: a REAL against /// the ACE double, one C2S game-action datagram dropped by the link — /// ACE NAKs the gap, the session resends from the cache on its Tick /// sweep, and every message dispatches in order with the crypto window /// intact. /// [Fact] public void LostGameAction_IsResentOnNak_AllMessagesDispatchInOrder() { var transport = new FakeAceTransport(); var session = new WorldSession( new IPEndPoint(IPAddress.Loopback, 9000), transport); try { session.Connect("testaccount", "testpassword", TimeSpan.FromSeconds(10)); session.EnterWorld(0, TimeSpan.FromSeconds(10)); Assert.Equal(WorldSession.State.InWorld, session.CurrentState); int baselineDispatched = transport.Model.DispatchedMessages.Count; // Ten game actions; the link eats C2S datagram #5 of the burst. // No Tick runs during the burst, so DropNext deterministically // hits the fifth SendTalk datagram. var expectedBodies = new List(); for (int i = 0; i < 10; i++) { if (i == 4) transport.Link.DropNext(LinkDirection.ClientToServer); string text = $"msg {i}"; expectedBodies.Add(ChatRequests.BuildTalk((uint)(i + 1), text)); session.SendTalk(text); } // Pump: the NAK is already queued S2C; Tick consumes it and the // end-of-Tick sweep resends the cached datagram. DateTime deadline = DateTime.UtcNow.AddSeconds(10); while (transport.Model.DispatchedMessages.Count < baselineDispatched + 10 && DateTime.UtcNow < deadline) { session.Tick(); Thread.Sleep(5); } // All ten dispatched, byte-identical, in fragment order. Assert.Equal( expectedBodies, transport.Model.DispatchedMessages .Skip(baselineDispatched) .ToList()); // Exactly one resend healed exactly one loss. Assert.Equal(1, transport.Link.DroppedCount(LinkDirection.ClientToServer)); Assert.Equal(1, session.Transport!.Stats.ResendsSent); Assert.Equal(1, session.Transport.Stats.NakRequestsReceived); Assert.Equal(0, session.Transport.Stats.UncachedNakIds); // The session survived and the crypto window is intact. Assert.False(transport.Model.IsTerminated); Assert.Equal(0, transport.Model.CrcDropCount); Assert.Equal(0, transport.Model.DuplicateDropCount); Assert.Equal(256, transport.Model.Crypto.Headroom); Assert.Equal(0, transport.Model.Crypto.OrphanCount); } finally { session.Dispose(); } Assert.Equal(WorldSession.State.Disconnected, session.CurrentState); } // ===================================================================== // Zero-alloc steady state // ===================================================================== [Fact] public void SendGameMessage_SteadyState_AllocatesNothingOnceThePoolWarms() { var stats = new TransportStats(); var virtualClock = new VirtualClock(); var clock = new TransportClock( virtualClock.GetTimestamp, virtualClock.Frequency); var queue = new OutboundFlowQueue( MakeIsaac(ClientSeed), (ushort)ClientId, SessionIteration, clock, stats, static _ => { }); byte[] body = MakeMessage(0x42); // Warm the pool + queue/dictionary capacity in the same // send → ack → sweep rhythm the measurement uses. for (int i = 0; i < 128; i++) { queue.SendGameMessage(body, GameMessageGroup.UIQueue); queue.OnAckSequence(queue.HighestIdSent); queue.TransmitPendingResends(); } long before = GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 1_000; i++) { queue.SendGameMessage(body, GameMessageGroup.UIQueue); queue.OnAckSequence(queue.HighestIdSent); queue.TransmitPendingResends(); } long allocated = GC.GetAllocatedBytesForCurrentThread() - before; Assert.Equal(0, allocated); queue.Dispose(); } // ===================================================================== // Fixture helpers // ===================================================================== private (OutboundFlowQueue Queue, VirtualClock VirtualClock, TransportClock Clock, TransportStats Stats, List Sent) CreateQueue() { var virtualClock = new VirtualClock(); var clock = new TransportClock( virtualClock.GetTimestamp, virtualClock.Frequency); var stats = new TransportStats(); var sent = new List(); var queue = new OutboundFlowQueue( MakeIsaac(ClientSeed), (ushort)ClientId, SessionIteration, clock, stats, datagram => sent.Add(datagram.ToArray())); return (queue, virtualClock, clock, stats, sent); } /// Deliver a NAK id list the way ProcessDatagram does: raw /// little-endian u32 ids + count. private static void Nak(OutboundFlowQueue queue, params uint[] ids) { byte[] bytes = new byte[ids.Length * 4]; for (int i = 0; i < ids.Length; i++) BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(i * 4), ids[i]); queue.OnRetransmitRequest(bytes, ids.Length); } /// An 8-byte message body whose first byte is a test marker. private static byte[] MakeMessage(byte marker) => new byte[] { marker, 0x11, 0x22, 0x33, 0x00, 0x00, 0x00, 0x00 }; private static IsaacRandom MakeIsaac(uint seed) { Span seedBytes = stackalloc byte[4]; BinaryPrimitives.WriteUInt32LittleEndian(seedBytes, seed); return new IsaacRandom(seedBytes); } /// A negotiated ACE double, matching the AceSessionModelTests /// fixture: LoginRequest → ConnectRequest (discarded) → ConnectResponse /// → immediate first TimeSync (discarded; S2C seq 2). private static (AceSessionModel Model, VirtualClock Clock) CreateNegotiatedModel() { var clock = new VirtualClock(); var model = new AceSessionModel(clock, ClientSeed, ServerSeed, ClientId, Cookie); model.LoginRequestReceived += model.SendConnectRequest; byte[] login = PacketCodec.Encode( new PacketHeader { Flags = PacketHeaderFlags.LoginRequest }, LoginRequest.Build("testaccount", "testpassword", 1234), outboundIsaac: null); model.Receive(login); model.Update(); model.TakePendingDatagrams(); byte[] cookieBody = new byte[8]; BinaryPrimitives.WriteUInt64LittleEndian(cookieBody, Cookie); byte[] connectResponse = PacketCodec.Encode( new PacketHeader { Sequence = 1, Flags = PacketHeaderFlags.ConnectResponse }, cookieBody, outboundIsaac: null); model.Receive(connectResponse); model.Update(); model.TakePendingDatagrams(); return (model, clock); } /// /// Recover the ISAAC word from an encrypted datagram's checksum: /// key = (checksum − headerHash) ^ payloadHash (ClientPacket.cs:142). /// private static uint ExtractIsaacKey(byte[] datagram) { PacketHeader header = PacketHeader.Unpack(datagram); ReadOnlySpan body = datagram.AsSpan(PacketHeader.Size, header.DataSize); var optional = new PacketHeaderOptional(); int consumed = optional.Parse(body, header.Flags); Assert.True(consumed >= 0); uint payloadHash = optional.CalculateHash32(); if ((header.Flags & PacketHeaderFlags.BlobFragments) != 0) { ReadOnlySpan remaining = body.Slice(consumed); while (!remaining.IsEmpty) { (MessageFragment? fragment, int fragmentBytes) = MessageFragment.TryParse(remaining); Assert.NotNull(fragment); payloadHash += PacketCodec.CalculateFragmentHash32(fragment!.Value); remaining = remaining.Slice(fragmentBytes); } } return (header.Checksum - header.CalculateHeaderHash32()) ^ payloadHash; } /// A cache entry whose buffer is rented from /// , so rent/return balance is assertable. private static SentPacketStore.CachedPacket RentedEntry( CountingPool pool, uint sequence) { byte[] buffer = pool.Rent(PacketHeader.Size + 24); return new SentPacketStore.CachedPacket( sequence, buffer, bodyLength: 24, sealedChecksum: 0u, isaacKey: 0u, hasFragments: true); } /// ArrayPool wrapper counting rents/returns for balance /// assertions. private sealed class CountingPool : ArrayPool { public int Rented { get; private set; } public int Returned { get; private set; } public override byte[] Rent(int minimumLength) { Rented++; return Shared.Rent(minimumLength); } public override void Return(byte[] array, bool clearArray = false) { Returned++; Shared.Return(array, clearArray); } } }