Ctrl K
ComunidadGuías…

Ferrite.Data/Repositories/SecretChatsRepository.cs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2022-2026 Aykut Alparslan KOC

using System.Security.Cryptography;
using Ferrite.TL;
using TLDto = Ferrite.TL.baseLayer.dto;

namespace Ferrite.Data.Repositories;

public sealed class SecretChatsRepository : ISecretChatsRepository
{
    private const int StripeCount = 256;
    private readonly IKVStore _chats;
    private readonly IKVStore _requestedDevices;
    private readonly IKVStore _pendingPairs;
    private readonly IKVStore _qtsEntries;
    private readonly IKVStore _qtsStates;
    private readonly IKVStore _sendReceipts;
    private readonly IKVStore _encryptedFiles;
    private readonly IKVStore _encryptedFileAssociations;
    private readonly IKVStore _controlUpdates;
    private readonly IKVStore _controlTargets;
    private readonly IKVStore _revocations;
    private readonly Func<ValueTask<bool>> _flush;
    private readonly SemaphoreSlim[] _chatGates = CreateGates();
    private readonly SemaphoreSlim[] _authKeyGates = CreateGates();
    private readonly SemaphoreSlim[] _fileGates = CreateGates();

    public SecretChatsRepository(IKVStore chats, IKVStore requestedDevices,
        IKVStore pendingPairs, IKVStore qtsEntries, IKVStore qtsStates,
        IKVStore sendReceipts,
        IKVStore encryptedFiles, IKVStore encryptedFileAssociations,
        IKVStore controlUpdates, IKVStore controlTargets, IKVStore revocations,
        Func<ValueTask<bool>>? flush = null)
    {
        _chats = chats;
        chats.SetSchema(new TableDefinition("ferrite", "secret_chats",
            new KeyDefinition("pk",
                new DataColumn { Name = "chat_id", Type = DataType.Int },
                new DataColumn { Name = "initiator_auth_key_id", Type = DataType.Long },
                new DataColumn { Name = "recipient_auth_key_id", Type = DataType.Long },
                new DataColumn { Name = "initiator_user_id", Type = DataType.Long },
                new DataColumn { Name = "random_id", Type = DataType.Int }),
            new KeyDefinition("by_initiator_auth_key",
                new DataColumn { Name = "initiator_auth_key_id", Type = DataType.Long },
                new DataColumn { Name = "chat_id", Type = DataType.Int }),
            new KeyDefinition("by_recipient_auth_key",
                new DataColumn { Name = "recipient_auth_key_id", Type = DataType.Long },
                new DataColumn { Name = "chat_id", Type = DataType.Int }),
            new KeyDefinition("by_initiator_random_id",
                new DataColumn { Name = "initiator_user_id", Type = DataType.Long },
                new DataColumn { Name = "random_id", Type = DataType.Int }),
            new KeyDefinition("by_initiator_user",
                new DataColumn { Name = "initiator_user_id", Type = DataType.Long },
                new DataColumn { Name = "chat_id", Type = DataType.Int })));

        _requestedDevices = requestedDevices;
        requestedDevices.SetSchema(new TableDefinition("ferrite",
            "secret_chat_requested_devices",
            new KeyDefinition("pk",
                new DataColumn { Name = "auth_key_id", Type = DataType.Long },
                new DataColumn { Name = "chat_id", Type = DataType.Int })));

        _pendingPairs = pendingPairs;
        pendingPairs.SetSchema(new TableDefinition("ferrite",
            "secret_chat_pending_pairs",
            new KeyDefinition("pk",
                new DataColumn { Name = "initiator_user_id", Type = DataType.Long },
                new DataColumn { Name = "recipient_user_id", Type = DataType.Long })));

        _qtsEntries = qtsEntries;
        qtsEntries.SetSchema(new TableDefinition("ferrite", "secret_chat_qts_entries",
            new KeyDefinition("pk",
                new DataColumn { Name = "recipient_auth_key_id", Type = DataType.Long },
                new DataColumn { Name = "qts", Type = DataType.Int })));

        _qtsStates = qtsStates;
        qtsStates.SetSchema(new TableDefinition("ferrite", "secret_chat_qts_state",
            new KeyDefinition("pk",
                new DataColumn { Name = "auth_key_id", Type = DataType.Long })));

        _sendReceipts = sendReceipts;
        sendReceipts.SetSchema(new TableDefinition("ferrite", "secret_chat_send_receipts",
            new KeyDefinition("pk",
                new DataColumn { Name = "chat_id", Type = DataType.Int },
                new DataColumn { Name = "sender_auth_key_id", Type = DataType.Long },
                new DataColumn { Name = "random_id", Type = DataType.Long })));

        _encryptedFiles = encryptedFiles;
        encryptedFiles.SetSchema(new TableDefinition("ferrite", "secret_chat_files",
            new KeyDefinition("pk",
                new DataColumn { Name = "file_id", Type = DataType.Long },
                new DataColumn { Name = "access_hash", Type = DataType.Long },
                new DataColumn { Name = "upload_file_id", Type = DataType.Long }),
            new KeyDefinition("by_exact",
                new DataColumn { Name = "file_id", Type = DataType.Long },
                new DataColumn { Name = "access_hash", Type = DataType.Long }),
            new KeyDefinition("by_id",
                new DataColumn { Name = "file_id", Type = DataType.Long }),
            new KeyDefinition("by_upload",
                new DataColumn { Name = "upload_file_id", Type = DataType.Long })));

        _encryptedFileAssociations = encryptedFileAssociations;
        encryptedFileAssociations.SetSchema(new TableDefinition("ferrite",
            "secret_chat_file_associations",
            new KeyDefinition("pk",
                new DataColumn { Name = "file_id", Type = DataType.Long },
                new DataColumn { Name = "chat_id", Type = DataType.Int }),
            new KeyDefinition("by_chat",
                new DataColumn { Name = "chat_id", Type = DataType.Int },
                new DataColumn { Name = "file_id", Type = DataType.Long })));

        _controlUpdates = controlUpdates;
        controlUpdates.SetSchema(new TableDefinition("ferrite",
            "secret_chat_control_updates",
            new KeyDefinition("pk",
                new DataColumn { Name = "recipient_auth_key_id", Type = DataType.Long },
                new DataColumn { Name = "update_id", Type = DataType.Long })));

        _controlTargets = controlTargets;
        controlTargets.SetSchema(new TableDefinition("ferrite",
            "secret_chat_control_targets",
            new KeyDefinition("pk",
                new DataColumn { Name = "auth_key_id", Type = DataType.Long },
                new DataColumn { Name = "chat_id", Type = DataType.Int }),
            new KeyDefinition("by_chat",
                new DataColumn { Name = "chat_id", Type = DataType.Int },
                new DataColumn { Name = "auth_key_id", Type = DataType.Long })));

        _revocations = revocations;
        _flush = flush ?? (() => ValueTask.FromResult(true));
        revocations.SetSchema(new TableDefinition("ferrite",
            "secret_chat_auth_key_revocations",
            new KeyDefinition("pk",
                new DataColumn { Name = "auth_key_id", Type = DataType.Long })));
    }

    public async ValueTask<SecretChatCreateResult> TryCreateChatAsync(
        TLDto.TLSecretChatState chat, int maxPendingChatsPerAuthKey = int.MaxValue,
        int maxOutstandingRequestsPerAuthKey = int.MaxValue,
        CancellationToken cancellationToken = default)
    {
        int chatId = ChatId(chat);
        long initiatorAuthKeyId = InitiatorAuthKeyId(chat);
        long initiatorUserId = InitiatorUserId(chat);
        long recipientUserId = RecipientUserId(chat);
        long[] requestedAuthKeys = RequestedRecipientAuthKeyIds(chat).Distinct()
            .ToArray();
        IReadOnlyList<SemaphoreSlim> authGates = await AcquireAuthKeyGatesAsync(
            requestedAuthKeys.Append(initiatorAuthKeyId), cancellationToken);
        SemaphoreSlim chatGate = GetGate(_chatGates, chatId);
        await chatGate.WaitAsync(cancellationToken);
        try
        {
            if (await IsRevokedAsync(initiatorAuthKeyId))
            {
                return new SecretChatCreateResult(SecretChatCreateStatus.AuthKeyRevoked, null);
            }

            TLDto.TLSecretChatState? byId = await GetChatInternalAsync(chatId,
                cancellationToken);
            if (byId is not null)
            {
                bool idempotent = IsIdempotentCreate(byId.Value, chat);
                return new SecretChatCreateResult(idempotent
                    ? SecretChatCreateStatus.Idempotent
                    : SecretChatCreateStatus.ChatIdCollision, byId.Value);
            }

            TLDto.TLSecretChatState? byRandom =
                await GetChatByInitiatorRandomIdInternalAsync(
                    initiatorUserId, chatId, cancellationToken);
            if (byRandom is not null)
            {
                return new SecretChatCreateResult(
                    SecretChatCreateStatus.InitiatorRandomIdCollision, byRandom.Value);
            }

            TLDto.TLSecretChatPendingPair? pendingPair = await GetPendingPairAsync(
                initiatorUserId, recipientUserId);
            if (pendingPair is not null)
            {
                int pendingChatId;
                using (TLDto.TLSecretChatPendingPair ownedPair = pendingPair.Value)
                {
                    pendingChatId = ownedPair.AsSecretChatPendingPair().ChatId;
                }
                TLDto.TLSecretChatState? pendingChat = await GetChatInternalAsync(
                    pendingChatId, cancellationToken);
                if (pendingChat is not null)
                {
                    TLDto.TLSecretChatState ownedPendingChat = pendingChat.Value;
                    if (State(ownedPendingChat) == SecretChatPersistenceState.Pending)
                    {
                        return new SecretChatCreateResult(
                            SecretChatCreateStatus.PairRequestExists,
                            ownedPendingChat);
                    }
                }
                _pendingPairs.Delete(initiatorUserId, recipientUserId);
            }

            TLDto.TLSecretChatState? indexedPending =
                await GetPendingChatForPairAsync(initiatorUserId, recipientUserId,
                    cancellationToken);
            if (indexedPending is not null)
            {
                TLDto.TLSecretChatState ownedPending = indexedPending.Value;
                using TLDto.TLSecretChatPendingPair repairedPair =
                    BuildPendingPair(ownedPending);
                _pendingPairs.Put(repairedPair.AsSpan().ToArray(), initiatorUserId,
                    recipientUserId);
                await FlushAsync("secret-chat pending-pair repair");
                return new SecretChatCreateResult(
                    SecretChatCreateStatus.PairRequestExists, ownedPending);
            }

            if (await CountChatsByStateAsync(initiatorAuthKeyId,
                    SecretChatPersistenceState.Pending, cancellationToken) >=
                Math.Max(0, maxPendingChatsPerAuthKey))
            {
                return new SecretChatCreateResult(
                    SecretChatCreateStatus.PendingLimitExceeded, null);
            }

            List<long> eligibleKeys = new();
            foreach (long authKeyId in requestedAuthKeys)
            {
                if (!await IsRevokedAsync(authKeyId))
                {
                    eligibleKeys.Add(authKeyId);
                }
            }
            if (eligibleKeys.Count == 0)
            {
                return new SecretChatCreateResult(
                    SecretChatCreateStatus.RecipientUnavailable, null);
            }
            foreach (long authKeyId in eligibleKeys)
            {
                if (await CountRequestedChatsAsync(authKeyId, cancellationToken) >=
                    Math.Max(0, maxOutstandingRequestsPerAuthKey))
                {
                    return new SecretChatCreateResult(
                        SecretChatCreateStatus.RecipientRequestLimitExceeded, null);
                }
            }

            TLDto.TLSecretChatState stored = BuildPendingChat(chat, eligibleKeys);
            try
            {
                PutChat(stored);
                using (TLDto.TLSecretChatPendingPair pair = BuildPendingPair(stored))
                {
                    _pendingPairs.Put(pair.AsSpan().ToArray(), initiatorUserId,
                        recipientUserId);
                }
                foreach (long authKeyId in eligibleKeys)
                {
                    using TLDto.TLSecretChatRequestedDevice target =
                        BuildRequestedDevice(authKeyId, ChatId(stored),
                            RecipientUserId(stored), CreatedAt(stored));
                    _requestedDevices.Put(target.AsSpan().ToArray(), authKeyId,
                        ChatId(stored));
                    using TLDto.TLSecretChatControlTarget controlTarget =
                        BuildControlTarget(authKeyId, ChatId(stored),
                            RecipientUserId(stored), CreatedAt(stored));
                    _controlTargets.Put(controlTarget.AsSpan().ToArray(), authKeyId,
                        ChatId(stored));
                }
                await FlushAsync("secret-chat request");
            }
            catch
            {
                stored.Dispose();
                throw;
            }

            return new SecretChatCreateResult(SecretChatCreateStatus.Created, stored);
        }
        finally
        {
            chatGate.Release();
            ReleaseGates(authGates);
        }
    }

    public ValueTask<TLDto.TLSecretChatState?> GetChatAsync(int chatId,
        CancellationToken cancellationToken = default) =>
        GetChatInternalAsync(chatId, cancellationToken);

    public ValueTask<TLDto.TLSecretChatState?> GetChatByInitiatorRandomIdAsync(
        long initiatorUserId, int randomId, CancellationToken cancellationToken = default) =>
        GetChatByInitiatorRandomIdInternalAsync(initiatorUserId, randomId,
            cancellationToken);

    public async ValueTask<IReadOnlyList<TLDto.TLSecretChatState>> GetChatsByAuthKeyAsync(
        long authKeyId, CancellationToken cancellationToken = default)
    {
        Dictionary<int, TLDto.TLSecretChatState> chats = new();
        await foreach (byte[] bytes in _chats.IterateBySecondaryIndexAsync(
                           "by_initiator_auth_key", authKeyId)
                           .WithCancellation(cancellationToken))
        {
            TLDto.TLSecretChatState row = ReadChat(bytes);
            chats[ChatId(row)] = row;
        }
        await foreach (byte[] bytes in _chats.IterateBySecondaryIndexAsync(
                           "by_recipient_auth_key", authKeyId)
                           .WithCancellation(cancellationToken))
        {
            TLDto.TLSecretChatState row = ReadChat(bytes);
            chats[ChatId(row)] = row;
        }
        return chats.Values.OrderBy(ChatId).ToArray();
    }

    public async ValueTask<IReadOnlyList<long>> GetRequestedRecipientAuthKeysAsync(
        int chatId, CancellationToken cancellationToken = default)
    {
        TLDto.TLSecretChatState? chat = await GetChatInternalAsync(chatId,
            cancellationToken);
        return chat is null ? [] : RequestedRecipientAuthKeyIds(chat.Value);
    }

    public async ValueTask<SecretChatAcceptResult> TryAcceptChatAsync(int chatId,
        long recipientAuthKeyId, ReadOnlyMemory<byte> gB, long keyFingerprint, int date,
        int maxActiveChatsPerAuthKey = int.MaxValue,
        CancellationToken cancellationToken = default)
    {
        byte[] gBCopy = gB.ToArray();
        TLDto.TLSecretChatState? initial = await GetChatInternalAsync(chatId,
            cancellationToken);
        if (initial is null)
        {
            return new SecretChatAcceptResult(SecretChatAcceptStatus.NotFound, null, []);
        }
        long[] transitionAuthKeyIds;
        using (TLDto.TLSecretChatState initialChat = initial.Value)
        {
            transitionAuthKeyIds = RequestedRecipientAuthKeyIds(initialChat)
                .Append(InitiatorAuthKeyId(initialChat))
                .Append(recipientAuthKeyId)
                .Distinct()
                .ToArray();
        }
        IReadOnlyList<SemaphoreSlim> authGates = await AcquireAuthKeyGatesAsync(
            transitionAuthKeyIds, cancellationToken);
        SemaphoreSlim chatGate = GetGate(_chatGates, chatId);
        await chatGate.WaitAsync(cancellationToken);
        try
        {
            TLDto.TLSecretChatState? existing = await GetChatInternalAsync(chatId,
                cancellationToken);
            if (existing is null)
            {
                return new SecretChatAcceptResult(SecretChatAcceptStatus.NotFound, null, []);
            }
            TLDto.TLSecretChatState existingChat = existing.Value;
            if (State(existingChat) != SecretChatPersistenceState.Pending)
            {
                return new SecretChatAcceptResult(SecretChatAcceptStatus.NotPending,
                    existingChat, []);
            }
            long[] requestedAuthKeys = RequestedRecipientAuthKeyIds(existingChat);
            if (!requestedAuthKeys.Contains(recipientAuthKeyId))
            {
                return new SecretChatAcceptResult(
                    SecretChatAcceptStatus.RecipientNotRequested, existingChat, []);
            }
            if (await IsRevokedAsync(recipientAuthKeyId))
            {
                return new SecretChatAcceptResult(SecretChatAcceptStatus.AuthKeyRevoked,
                    existingChat, []);
            }

            long initiatorAuthKeyId = InitiatorAuthKeyId(existingChat);
            if (await IsRevokedAsync(initiatorAuthKeyId))
            {
                return new SecretChatAcceptResult(SecretChatAcceptStatus.AuthKeyRevoked,
                    existingChat, []);
            }
            int activeLimit = Math.Max(0, maxActiveChatsPerAuthKey);
            if (await CountChatsByStateAsync(initiatorAuthKeyId,
                    SecretChatPersistenceState.Active, cancellationToken) >= activeLimit ||
                await CountChatsByStateAsync(recipientAuthKeyId,
                    SecretChatPersistenceState.Active, cancellationToken) >= activeLimit)
            {
                return new SecretChatAcceptResult(
                    SecretChatAcceptStatus.ActiveLimitExceeded, existingChat, []);
            }

            long[] losingKeys = requestedAuthKeys
                .Where(x => x != recipientAuthKeyId).Distinct().ToArray();
            TLDto.TLSecretChatState accepted = BuildAcceptedChat(existingChat,
                recipientAuthKeyId, gBCopy, keyFingerprint, date);
            try
            {
                ReplaceChat(existingChat, accepted);
                await DeleteControlTargetsByChatAsync(chatId, cancellationToken);
                DeleteRequestedDevices(existingChat);
                using (TLDto.TLSecretChatControlTarget initiatorTarget =
                       BuildControlTarget(initiatorAuthKeyId, chatId,
                           InitiatorUserId(existingChat), date))
                {
                    _controlTargets.Put(initiatorTarget.AsSpan().ToArray(),
                        initiatorAuthKeyId, chatId);
                }
                foreach (long losingKey in losingKeys)
                {
                    using TLDto.TLSecretChatControlTarget losingTarget =
                        BuildControlTarget(losingKey, chatId,
                            RecipientUserId(existingChat), date);
                    _controlTargets.Put(losingTarget.AsSpan().ToArray(), losingKey,
                        chatId);
                }
                await FlushAsync("secret-chat acceptance");
            }
            catch
            {
                accepted.Dispose();
                throw;
            }
            return new SecretChatAcceptResult(SecretChatAcceptStatus.Accepted, accepted,
                losingKeys);
        }
        finally
        {
            chatGate.Release();
            ReleaseGates(authGates);
        }
    }

    public async ValueTask<SecretChatDiscardResult> TryDiscardChatAsync(int chatId,
        long callerAuthKeyId, bool deleteHistory, int date,
        CancellationToken cancellationToken = default)
    {
        TLDto.TLSecretChatState? initial = await GetChatInternalAsync(chatId,
            cancellationToken);
        if (initial is null)
        {
            return new SecretChatDiscardResult(SecretChatDiscardStatus.NotFound, null,
                []);
        }
        long[] transitionAuthKeyIds;
        SecretChatPersistenceState initialState;
        using (TLDto.TLSecretChatState initialChat = initial.Value)
        {
            TLDto.SecretChatState initialRow = initialChat.AsSecretChatState();
            initialState = State(initialChat);
            transitionAuthKeyIds = RequestedRecipientAuthKeyIds(initialChat)
                .Append(initialRow.InitiatorAuthKeyId)
                .Append(initialRow.Flags[1] ? initialRow.RecipientAuthKeyId : 0L)
                .Append(callerAuthKeyId)
                .Where(x => x != 0)
                .Distinct()
                .ToArray();
        }
        IReadOnlyList<SemaphoreSlim> authGates = await AcquireAuthKeyGatesAsync(
            transitionAuthKeyIds, cancellationToken);
        SemaphoreSlim chatGate = GetGate(_chatGates, chatId);
        await chatGate.WaitAsync(cancellationToken);
        try
        {
            TLDto.TLSecretChatState? existing = await GetChatInternalAsync(chatId,
                cancellationToken);
            if (existing is null)
            {
                return new SecretChatDiscardResult(SecretChatDiscardStatus.NotFound, null,
                    []);
            }
            TLDto.TLSecretChatState existingChat = existing.Value;
            TLDto.SecretChatState row = existingChat.AsSecretChatState();
            long[] requestedAuthKeys = RequestedRecipientAuthKeyIds(existingChat);
            long? recipientAuthKeyId = row.Flags[1] ? row.RecipientAuthKeyId : null;
            SecretChatPersistenceState state = State(existingChat);
            bool isInitiator = row.InitiatorAuthKeyId == callerAuthKeyId;
            bool isRequestedRecipient = requestedAuthKeys.Contains(callerAuthKeyId);
            bool isBoundRecipient = recipientAuthKeyId == callerAuthKeyId;
            if (state == SecretChatPersistenceState.Discarded)
            {
                SecretChatDiscardStatus terminalStatus;
                if (recipientAuthKeyId is not null && isRequestedRecipient &&
                    !isBoundRecipient && !isInitiator)
                {
                    terminalStatus = SecretChatDiscardStatus.AlreadyAccepted;
                }
                else if (isInitiator || isBoundRecipient ||
                         recipientAuthKeyId is null && isRequestedRecipient)
                {
                    terminalStatus = SecretChatDiscardStatus.AlreadyDiscarded;
                }
                else
                {
                    terminalStatus = SecretChatDiscardStatus.Unauthorized;
                }
                return new SecretChatDiscardResult(terminalStatus, existingChat, []);
            }
            if (initialState == SecretChatPersistenceState.Pending &&
                state == SecretChatPersistenceState.Active)
            {
                return new SecretChatDiscardResult(
                    SecretChatDiscardStatus.AlreadyAccepted, existingChat, []);
            }
            if (state == SecretChatPersistenceState.Active && isRequestedRecipient &&
                !isBoundRecipient && !isInitiator)
            {
                return new SecretChatDiscardResult(
                    SecretChatDiscardStatus.AlreadyAccepted, existingChat, []);
            }
            if ((state == SecretChatPersistenceState.Pending &&
                 !isInitiator && !isRequestedRecipient) ||
                (state == SecretChatPersistenceState.Active &&
                 !isInitiator && !isBoundRecipient))
            {
                return new SecretChatDiscardResult(
                    SecretChatDiscardStatus.Unauthorized, existingChat, []);
            }

            long[] notificationAuthKeyIds = state == SecretChatPersistenceState.Pending
                ? requestedAuthKeys.Append(row.InitiatorAuthKeyId)
                    .Where(x => x != callerAuthKeyId).Distinct().ToArray()
                : new long[] { row.InitiatorAuthKeyId, recipientAuthKeyId!.Value }
                    .Where(x => x != callerAuthKeyId).Distinct().ToArray();
            long initiatorAuthKeyId = row.InitiatorAuthKeyId;
            long initiatorUserId = row.InitiatorUserId;
            long recipientUserId = row.RecipientUserId;

            TLDto.TLSecretChatState discarded = BuildDiscardedChat(existingChat,
                deleteHistory, date);
            try
            {
                ReplaceChat(existingChat, discarded);
                await DeleteControlTargetsByChatAsync(chatId, cancellationToken);
                DeleteRequestedDevices(existingChat);
                foreach (long notificationAuthKeyId in notificationAuthKeyIds)
                {
                    long notificationUserId = notificationAuthKeyId ==
                        initiatorAuthKeyId
                            ? initiatorUserId
                            : recipientUserId;
                    using TLDto.TLSecretChatControlTarget target = BuildControlTarget(
                        notificationAuthKeyId, chatId, notificationUserId, date);
                    _controlTargets.Put(target.AsSpan().ToArray(),
                        notificationAuthKeyId, chatId);
                }
                await FlushAsync("secret-chat discard");
            }
            catch
            {
                discarded.Dispose();
                throw;
            }
            return new SecretChatDiscardResult(SecretChatDiscardStatus.Discarded,
                discarded, notificationAuthKeyIds);
        }
        finally
        {
            chatGate.Release();
            ReleaseGates(authGates);
        }
    }

    public async ValueTask<IReadOnlyList<TLDto.TLSecretChatState>>
        GetControlTargetChatsAsync(long authKeyId,
            CancellationToken cancellationToken = default)
    {
        var chats = new Dictionary<int, TLDto.TLSecretChatState>();
        await foreach (byte[] bytes in _controlTargets.IterateAsync(authKeyId)
                           .WithCancellation(cancellationToken))
        {
            using TLDto.TLSecretChatControlTarget target =
                ReadControlTarget(bytes);
            int chatId = target.AsSecretChatControlTarget().ChatId;
            TLDto.TLSecretChatState? chat = await GetChatInternalAsync(chatId,
                cancellationToken);
            if (chat is not null)
            {
                chats[chatId] = chat.Value;
            }
        }
        return chats.Values.OrderBy(ChatId).ToArray();
    }

    public async ValueTask<IReadOnlyList<long>> GetControlTargetAuthKeyIdsAsync(
        int chatId, CancellationToken cancellationToken = default)
    {
        var authKeyIds = new HashSet<long>();
        await foreach (byte[] bytes in _controlTargets
                           .IterateBySecondaryIndexAsync("by_chat", chatId)
                           .WithCancellation(cancellationToken))
        {
            using TLDto.TLSecretChatControlTarget target =
                ReadControlTarget(bytes);
            authKeyIds.Add(target.AsSecretChatControlTarget().AuthKeyId);
        }
        return authKeyIds.Order().ToArray();
    }

    public async ValueTask<bool> CompleteControlTransitionAsync(int chatId,
        SecretChatPersistenceState expectedState, int expectedDate,
        CancellationToken cancellationToken = default)
    {
        SemaphoreSlim chatGate = GetGate(_chatGates, chatId);
        await chatGate.WaitAsync(cancellationToken);
        try
        {
            TLDto.TLSecretChatState? chat = await GetChatInternalAsync(chatId,
                cancellationToken);
            if (chat is null)
            {
                return false;
            }
            using TLDto.TLSecretChatState ownedChat = chat.Value;
            TLDto.SecretChatState row = ownedChat.AsSecretChatState();
            if ((SecretChatPersistenceState)row.State != expectedState ||
                row.UpdatedAt != expectedDate)
            {
                return false;
            }
            await DeleteControlTargetsByChatAsync(chatId, cancellationToken);
            await FlushAsync("secret-chat control-transition completion");
            return true;
        }
        finally
        {
            chatGate.Release();
        }
    }

    public async ValueTask<SecretChatReceiptPutResult> TryPutSendReceiptAsync(
        TLDto.TLSecretChatSendReceipt receipt,
        CancellationToken cancellationToken = default)
    {
        int chatId = receipt.AsSecretChatSendReceipt().ChatId;
        long senderAuthKeyId = receipt.AsSecretChatSendReceipt().SenderAuthKeyId;
        long randomId = receipt.AsSecretChatSendReceipt().RandomId;
        SemaphoreSlim gate = GetGate(_chatGates, chatId);
        await gate.WaitAsync(cancellationToken);
        try
        {
            TLDto.TLSecretChatSendReceipt? existing = await GetSendReceiptAsync(chatId,
                senderAuthKeyId, randomId, cancellationToken: cancellationToken);
            if (existing is not null)
            {
                return new SecretChatReceiptPutResult(
                    SecretChatReceiptPutStatus.AlreadyExists, existing.Value);
            }
            TLDto.TLSecretChatSendReceipt stored =
                receipt.AsSecretChatSendReceipt().Clone().Build();
            try
            {
                _sendReceipts.Put(stored.AsSpan().ToArray(), chatId, senderAuthKeyId,
                    randomId);
                await FlushAsync("secret-chat send receipt");
            }
            catch
            {
                stored.Dispose();
                throw;
            }
            return new SecretChatReceiptPutResult(SecretChatReceiptPutStatus.Created,
                stored);
        }
        finally
        {
            gate.Release();
        }
    }

    public async ValueTask<TLDto.TLSecretChatSendReceipt?> GetSendReceiptAsync(int chatId,
        long senderAuthKeyId, long randomId,
        int minimumDate = int.MinValue,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        byte[]? bytes = await _sendReceipts.GetAsync(chatId, senderAuthKeyId, randomId);
        if (bytes is not { Length: > 0 })
        {
            return null;
        }
        TLDto.TLSecretChatSendReceipt receipt = ReadSendReceipt(bytes);
        if (receipt.AsSecretChatSendReceipt().Date >= minimumDate)
        {
            return receipt;
        }
        receipt.Dispose();
        return null;
    }

    public async ValueTask<SecretChatReadAdvanceStatus> AdvanceReadDateAsync(
        long callerAuthKeyId, int chatId, long accessHash, int maxDate,
        TLDto.TLSecretChatControlUpdate controlUpdate,
        CancellationToken cancellationToken = default)
    {
        TLDto.SecretChatControlUpdate control =
            controlUpdate.AsSecretChatControlUpdate();
        long recipientAuthKeyId = control.RecipientAuthKeyId;
        long controlUpdateId = control.UpdateId;
        int controlChatId = control.ChatId;
        long controlPeerAuthKeyId = control.PeerAuthKeyId;
        long controlPeerUserId = control.PeerUserId;
        byte[] controlBytes = controlUpdate.AsSpan().ToArray();
        IReadOnlyList<SemaphoreSlim> authGates = await AcquireAuthKeyGatesAsync(
            new[] { callerAuthKeyId, recipientAuthKeyId }, cancellationToken);
        SemaphoreSlim chatGate = GetGate(_chatGates, chatId);
        await chatGate.WaitAsync(cancellationToken);
        try
        {
            if (await IsRevokedAsync(callerAuthKeyId) ||
                await IsRevokedAsync(recipientAuthKeyId))
            {
                return SecretChatReadAdvanceStatus.AuthKeyRevoked;
            }
            TLDto.TLSecretChatState? chatValue = await GetChatInternalAsync(chatId,
                cancellationToken);
            if (chatValue is null)
            {
                return SecretChatReadAdvanceStatus.NotFound;
            }
            using TLDto.TLSecretChatState chat = chatValue.Value;
            TLDto.SecretChatState row = chat.AsSecretChatState();
            if (row.AccessHash != accessHash)
            {
                return SecretChatReadAdvanceStatus.AccessHashInvalid;
            }
            if ((SecretChatPersistenceState)row.State !=
                SecretChatPersistenceState.Active || !row.Flags[1])
            {
                return SecretChatReadAdvanceStatus.NotActive;
            }

            bool callerIsInitiator = callerAuthKeyId == row.InitiatorAuthKeyId;
            bool callerIsRecipient = callerAuthKeyId == row.RecipientAuthKeyId;
            long expectedRecipientAuthKeyId = callerIsInitiator
                ? row.RecipientAuthKeyId
                : row.InitiatorAuthKeyId;
            long callerUserId = callerIsInitiator
                ? row.InitiatorUserId
                : row.RecipientUserId;
            if ((!callerIsInitiator && !callerIsRecipient) ||
                recipientAuthKeyId != expectedRecipientAuthKeyId ||
                controlChatId != chatId ||
                controlPeerAuthKeyId != callerAuthKeyId ||
                controlPeerUserId != callerUserId)
            {
                return SecretChatReadAdvanceStatus.Unauthorized;
            }

            int currentMaxDate = callerIsInitiator
                ? row.InitiatorReadMaxDate
                : row.RecipientReadMaxDate;
            if (maxDate <= currentMaxDate)
            {
                return SecretChatReadAdvanceStatus.Unchanged;
            }

            using TLDto.TLSecretChatState updated = BuildReadChat(chat,
                callerIsInitiator, maxDate);
            ReplaceChat(chat, updated);
            _controlUpdates.Put(controlBytes, recipientAuthKeyId, controlUpdateId);
            await FlushAsync("secret-chat read state and control update");
            return SecretChatReadAdvanceStatus.Advanced;
        }
        finally
        {
            chatGate.Release();
            ReleaseGates(authGates);
        }
    }

    public async ValueTask<SecretChatQtsAppendResult> AppendQtsAsync(
        long recipientAuthKeyId, int chatId, long randomId, int date, int expiresAt,
        ReadOnlyMemory<byte> encryptedMessage, int maxEvents, long maxBytes,
        Func<ValueTask<int>> getCurrentQts, Func<ValueTask<int>> incrementQts,
        CancellationToken cancellationToken = default)
    {
        byte[] messageCopy = encryptedMessage.ToArray();
        SemaphoreSlim gate = GetGate(_authKeyGates, recipientAuthKeyId);
        await gate.WaitAsync(cancellationToken);
        try
        {
            if (await IsRevokedAsync(recipientAuthKeyId))
            {
                return new SecretChatQtsAppendResult(
                    SecretChatQtsAppendStatus.AuthKeyRevoked, null);
            }
            TLDto.TLSecretChatQtsState state = await GetOrCreateQtsStateAsync(
                recipientAuthKeyId, cancellationToken);
            try
            {
                state = await RecoverPendingQtsAppendAsync(state, getCurrentQts,
                    incrementQts);
                ExpiredQtsResult expiry = await ExpireContiguousQtsAsync(state,
                    recipientAuthKeyId, date, cancellationToken);
                state = expiry.State;
                int lastPersistedQts = state.AsSecretChatQtsState().LastPersistedQts;
                int acknowledgedQts = state.AsSecretChatQtsState().AcknowledgedQts;
                int queuedEvents = state.AsSecretChatQtsState().QueuedEvents;
                long queuedBytes = state.AsSecretChatQtsState().QueuedBytes;
                if (queuedEvents >= maxEvents)
                {
                    return new SecretChatQtsAppendResult(
                        SecretChatQtsAppendStatus.EventLimitExceeded, null);
                }
                if (messageCopy.LongLength > maxBytes - queuedBytes)
                {
                    return new SecretChatQtsAppendResult(
                        SecretChatQtsAppendStatus.ByteLimitExceeded, null);
                }

                using TLDto.TLSecretChatQtsPending pending = BuildQtsPending(chatId,
                    randomId, date, expiresAt, messageCopy);
                using TLDto.TLSecretChatQtsState reserved = BuildQtsState(
                    recipientAuthKeyId, lastPersistedQts, acknowledgedQts, queuedEvents,
                    queuedBytes, pending);
                PutQtsState(reserved);
                await FlushAsync("secret-chat qts reservation");

                int qts = await incrementQts();
                if (qts <= lastPersistedQts)
                {
                    throw new InvalidOperationException(
                        "The canonical qts counter did not advance.");
                }
                TLDto.TLSecretChatQtsEntry entry = BuildQtsEntry(recipientAuthKeyId,
                    qts, chatId, randomId, date, expiresAt, messageCopy);
                try
                {
                    using TLDto.TLSecretChatQtsState next = BuildQtsState(
                        recipientAuthKeyId, qts, acknowledgedQts,
                        checked(queuedEvents + 1),
                        checked(queuedBytes + messageCopy.LongLength), null);
                    _qtsEntries.Put(entry.AsSpan().ToArray(), recipientAuthKeyId, qts);
                    PutQtsState(next);
                    await FlushAsync("secret-chat qts entry");
                }
                catch
                {
                    entry.Dispose();
                    throw;
                }
                return new SecretChatQtsAppendResult(SecretChatQtsAppendStatus.Appended,
                    entry);
            }
            finally
            {
                state.Dispose();
            }
        }
        finally
        {
            gate.Release();
        }
    }

    public async ValueTask<SecretChatSendAppendResult> AppendSendQtsAsync(
        long senderAuthKeyId, long recipientAuthKeyId, int chatId, long accessHash,
        long randomId, int date, int expiresAt,
        ReadOnlyMemory<byte> encryptedMessage, ReadOnlyMemory<byte> result,
        int maxEvents, long maxBytes, int receiptRetentionSeconds,
        Func<ValueTask<int>> getCurrentQts,
        Func<ValueTask<int>> incrementQts,
        CancellationToken cancellationToken = default)
    {
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(
            receiptRetentionSeconds);
        byte[] messageCopy = encryptedMessage.ToArray();
        byte[] resultCopy = result.ToArray();
        IReadOnlyList<SemaphoreSlim> authGates = await AcquireAuthKeyGatesAsync(
            new[] { senderAuthKeyId, recipientAuthKeyId }, cancellationToken);
        try
        {
            if (await IsRevokedAsync(senderAuthKeyId) ||
                await IsRevokedAsync(recipientAuthKeyId))
            {
                return new SecretChatSendAppendResult(
                    SecretChatSendAppendStatus.AuthKeyRevoked, null, null);
            }

            TLDto.TLSecretChatQtsState state = await GetOrCreateQtsStateAsync(
                recipientAuthKeyId, cancellationToken);
            try
            {
                state = await RecoverPendingQtsAppendAsync(state, getCurrentQts,
                    incrementQts);
                ExpiredQtsResult expiry = await ExpireContiguousQtsAsync(state,
                    recipientAuthKeyId, date, cancellationToken);
                state = expiry.State;

                SemaphoreSlim chatGate = GetGate(_chatGates, chatId);
                await chatGate.WaitAsync(cancellationToken);
                try
                {
                    TLDto.TLSecretChatState? chatValue = await GetChatInternalAsync(
                        chatId, cancellationToken);
                    if (chatValue is null)
                    {
                        return new SecretChatSendAppendResult(
                            SecretChatSendAppendStatus.NotFound, null, null);
                    }
                    using TLDto.TLSecretChatState chat = chatValue.Value;
                    TLDto.SecretChatState chatRow = chat.AsSecretChatState();
                    if (chatRow.AccessHash != accessHash)
                    {
                        return new SecretChatSendAppendResult(
                            SecretChatSendAppendStatus.AccessHashInvalid, null, null);
                    }
                    if ((SecretChatPersistenceState)chatRow.State !=
                        SecretChatPersistenceState.Active || !chatRow.Flags[1])
                    {
                        return new SecretChatSendAppendResult(
                            SecretChatSendAppendStatus.NotActive, null, null);
                    }
                    bool exactPair = senderAuthKeyId == chatRow.InitiatorAuthKeyId &&
                                     recipientAuthKeyId == chatRow.RecipientAuthKeyId ||
                                     senderAuthKeyId == chatRow.RecipientAuthKeyId &&
                                     recipientAuthKeyId == chatRow.InitiatorAuthKeyId;
                    if (!exactPair)
                    {
                        return new SecretChatSendAppendResult(
                            SecretChatSendAppendStatus.Unauthorized, null, null);
                    }

                    int receiptDate = date;
                    byte[] receiptResult = resultCopy;
                    bool repeated = false;
                    TLDto.TLSecretChatSendReceipt? existing =
                        await GetSendReceiptAsync(chatId, senderAuthKeyId, randomId,
                            cancellationToken: cancellationToken);
                    if (existing is not null)
                    {
                        using (TLDto.TLSecretChatSendReceipt existingReceipt =
                                   existing.Value)
                        {
                            TLDto.SecretChatSendReceipt existingRow =
                                existingReceipt.AsSecretChatSendReceipt();
                            if (existingRow.Date >=
                                checked(date - receiptRetentionSeconds))
                            {
                                receiptDate = existingRow.Date;
                                receiptResult = existingRow.Result.ToArray();
                                repeated = true;
                            }
                        }
                        if (!repeated)
                        {
                            await _sendReceipts.DeleteAsync(chatId, senderAuthKeyId,
                                randomId);
                        }
                    }

                    TLDto.SecretChatQtsState stateRow = state.AsSecretChatQtsState();
                    int lastPersistedQts = stateRow.LastPersistedQts;
                    int acknowledgedQts = stateRow.AcknowledgedQts;
                    int queuedEvents = stateRow.QueuedEvents;
                    long queuedBytes = stateRow.QueuedBytes;
                    if (queuedEvents >= maxEvents)
                    {
                        return new SecretChatSendAppendResult(
                            SecretChatSendAppendStatus.EventLimitExceeded, null, null);
                    }
                    if (messageCopy.LongLength > maxBytes - queuedBytes)
                    {
                        return new SecretChatSendAppendResult(
                            SecretChatSendAppendStatus.ByteLimitExceeded, null, null);
                    }

                    using TLDto.TLSecretChatSendQtsPending pending =
                        BuildSendQtsPending(senderAuthKeyId, chatId, randomId, date,
                            expiresAt, messageCopy, resultCopy);
                    using TLDto.TLSecretChatQtsState reserved = BuildQtsState(
                        recipientAuthKeyId, lastPersistedQts, acknowledgedQts,
                        queuedEvents, queuedBytes, null, pending);
                    PutQtsState(reserved);
                    await FlushAsync("secret-chat send qts reservation");

                    int qts = await incrementQts();
                    if (qts <= lastPersistedQts)
                    {
                        throw new InvalidOperationException(
                            "The canonical qts counter did not advance.");
                    }
                    TLDto.TLSecretChatQtsEntry entry = BuildQtsEntry(
                        recipientAuthKeyId, qts, chatId, randomId, date, expiresAt,
                        messageCopy);
                    TLDto.TLSecretChatSendReceipt receipt;
                    try
                    {
                        receipt = BuildSendReceipt(chatId, senderAuthKeyId, randomId,
                            receiptDate, qts, receiptResult);
                    }
                    catch
                    {
                        entry.Dispose();
                        throw;
                    }
                    try
                    {
                        using TLDto.TLSecretChatQtsState next = BuildQtsState(
                            recipientAuthKeyId, qts, acknowledgedQts,
                            checked(queuedEvents + 1),
                            checked(queuedBytes + messageCopy.LongLength), null, null);
                        _qtsEntries.Put(entry.AsSpan().ToArray(), recipientAuthKeyId,
                            qts);
                        _sendReceipts.Put(receipt.AsSpan().ToArray(), chatId,
                            senderAuthKeyId, randomId);
                        PutQtsState(next);
                        await FlushAsync("secret-chat send qts entry");
                    }
                    catch
                    {
                        receipt.Dispose();
                        entry.Dispose();
                        throw;
                    }
                    return new SecretChatSendAppendResult(repeated
                        ? SecretChatSendAppendStatus.Redelivered
                        : SecretChatSendAppendStatus.Appended, entry, receipt);
                }
                finally
                {
                    chatGate.Release();
                }
            }
            finally
            {
                state.Dispose();
            }
        }
        finally
        {
            ReleaseGates(authGates);
        }
    }

    public async ValueTask<IReadOnlyList<TLDto.TLSecretChatQtsEntry>> GetQtsEntriesAsync(
        long recipientAuthKeyId, int afterQts = 0, int limit = int.MaxValue,
        CancellationToken cancellationToken = default)
    {
        if (limit <= 0)
        {
            return [];
        }
        List<TLDto.TLSecretChatQtsEntry> entries = new();
        await foreach (byte[] bytes in _qtsEntries.IterateAsync(recipientAuthKeyId)
                           .WithCancellation(cancellationToken))
        {
            TLDto.TLSecretChatQtsEntry entry = ReadQtsEntry(bytes);
            if (entry.AsSecretChatQtsEntry().Qts > afterQts)
            {
                entries.Add(entry);
            }
        }
        return entries.OrderBy(x => x.AsSecretChatQtsEntry().Qts).Take(limit).ToArray();
    }

    public async ValueTask<TLDto.TLSecretChatQtsState> RecoverPendingQtsAsync(
        long recipientAuthKeyId, Func<ValueTask<int>> getCurrentQts,
        Func<ValueTask<int>> incrementQts,
        CancellationToken cancellationToken = default)
    {
        SemaphoreSlim gate = GetGate(_authKeyGates, recipientAuthKeyId);
        await gate.WaitAsync(cancellationToken);
        try
        {
            TLDto.TLSecretChatQtsState state = await GetOrCreateQtsStateAsync(
                recipientAuthKeyId, cancellationToken);
            try
            {
                return await RecoverPendingQtsAppendAsync(state, getCurrentQts,
                    incrementQts);
            }
            catch
            {
                state.Dispose();
                throw;
            }
        }
        finally
        {
            gate.Release();
        }
    }

    public async ValueTask<TLDto.TLSecretChatQtsState> AcknowledgeQtsAsync(
        long recipientAuthKeyId, int maxQts,
        CancellationToken cancellationToken = default)
    {
        SemaphoreSlim gate = GetGate(_authKeyGates, recipientAuthKeyId);
        await gate.WaitAsync(cancellationToken);
        try
        {
            TLDto.TLSecretChatQtsState state = await GetOrCreateQtsStateAsync(
                recipientAuthKeyId, cancellationToken);
            try
            {
                int lastPersistedQts = state.AsSecretChatQtsState().LastPersistedQts;
                int previousAcknowledgedQts =
                    state.AsSecretChatQtsState().AcknowledgedQts;
                int queuedEvents = state.AsSecretChatQtsState().QueuedEvents;
                long queuedBytes = state.AsSecretChatQtsState().QueuedBytes;
                int acknowledged = Math.Max(previousAcknowledgedQts,
                    Math.Min(maxQts, lastPersistedQts));
                IReadOnlyList<TLDto.TLSecretChatQtsEntry> entries =
                    await ReadQtsEntriesInternalAsync(recipientAuthKeyId,
                        cancellationToken);
                (int deletedEvents, long deletedBytes) = await DeleteQtsEntriesAsync(
                    recipientAuthKeyId, entries.Where(x =>
                        x.AsSecretChatQtsEntry().Qts <= acknowledged));
                TLDto.TLSecretChatQtsPending? pending = GetPendingAppend(state);
                TLDto.TLSecretChatSendQtsPending? pendingSend = GetPendingSend(state);
                TLDto.TLSecretChatQtsState next = BuildQtsState(recipientAuthKeyId,
                    lastPersistedQts, acknowledged,
                    Math.Max(0, queuedEvents - deletedEvents),
                    Math.Max(0, queuedBytes - deletedBytes), pending, pendingSend);
                try
                {
                    PutQtsState(next);
                    await FlushAsync("secret-chat qts acknowledgement");
                }
                catch
                {
                    next.Dispose();
                    throw;
                }
                return next;
            }
            finally
            {
                state.Dispose();
            }
        }
        finally
        {
            gate.Release();
        }
    }

    public async ValueTask<SecretChatQtsConfirmResult> ConfirmQtsAsync(
        long recipientAuthKeyId, int maxQts, Func<ValueTask<int>> getCurrentQts,
        Func<ValueTask<int>> incrementQts,
        CancellationToken cancellationToken = default)
    {
        SemaphoreSlim gate = GetGate(_authKeyGates, recipientAuthKeyId);
        await gate.WaitAsync(cancellationToken);
        try
        {
            TLDto.TLSecretChatQtsState state = await GetOrCreateQtsStateAsync(
                recipientAuthKeyId, cancellationToken);
            try
            {
                state = await RecoverPendingQtsAppendAsync(state, getCurrentQts,
                    incrementQts);
                int acknowledgedQts = state.AsSecretChatQtsState().AcknowledgedQts;
                int lastPersistedQts = state.AsSecretChatQtsState().LastPersistedQts;
                bool valid = maxQts == acknowledgedQts ||
                             maxQts > acknowledgedQts && maxQts <= lastPersistedQts;
                if (!valid)
                {
                    return new SecretChatQtsConfirmResult(
                        SecretChatQtsConfirmStatus.Invalid, state);
                }
                if (maxQts == acknowledgedQts)
                {
                    return new SecretChatQtsConfirmResult(
                        SecretChatQtsConfirmStatus.Confirmed, state);
                }

                IReadOnlyList<TLDto.TLSecretChatQtsEntry> entries =
                    await ReadQtsEntriesInternalAsync(recipientAuthKeyId,
                        cancellationToken);
                (int deletedEvents, long deletedBytes) = await DeleteQtsEntriesAsync(
                    recipientAuthKeyId, entries.Where(x =>
                        x.AsSecretChatQtsEntry().Qts <= maxQts));
                TLDto.TLSecretChatQtsState confirmed = BuildQtsState(
                    recipientAuthKeyId, lastPersistedQts, maxQts,
                    Math.Max(0,
                        state.AsSecretChatQtsState().QueuedEvents - deletedEvents),
                    Math.Max(0,
                        state.AsSecretChatQtsState().QueuedBytes - deletedBytes),
                    null);
                try
                {
                    PutQtsState(confirmed);
                    await FlushAsync("secret-chat qts confirmation");
                }
                catch
                {
                    confirmed.Dispose();
                    throw;
                }
                state.Dispose();
                state = confirmed;
                return new SecretChatQtsConfirmResult(
                    SecretChatQtsConfirmStatus.Confirmed, confirmed);
            }
            catch
            {
                state.Dispose();
                throw;
            }
        }
        finally
        {
            gate.Release();
        }
    }

    public async ValueTask<SecretChatQtsDifferenceResult> ReadQtsDifferenceAsync(
        long recipientAuthKeyId, int requestQts, int now, int limit,
        Func<ValueTask<int>> getCurrentQts, Func<ValueTask<int>> incrementQts,
        CancellationToken cancellationToken = default)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(limit);
        SemaphoreSlim gate = GetGate(_authKeyGates, recipientAuthKeyId);
        await gate.WaitAsync(cancellationToken);
        try
        {
            TLDto.TLSecretChatQtsState state = await GetOrCreateQtsStateAsync(
                recipientAuthKeyId, cancellationToken);
            try
            {
                state = await RecoverPendingQtsAppendAsync(state, getCurrentQts,
                    incrementQts);
                int lastPersistedQts = state.AsSecretChatQtsState().LastPersistedQts;
                int acknowledgedQts = state.AsSecretChatQtsState().AcknowledgedQts;
                int queuedEvents = state.AsSecretChatQtsState().QueuedEvents;
                long queuedBytes = state.AsSecretChatQtsState().QueuedBytes;
                List<TLDto.TLSecretChatQtsEntry> entries =
                    (await ReadQtsEntriesInternalAsync(recipientAuthKeyId,
                        cancellationToken)).ToList();
                var deletedQts = new HashSet<int>();
                long deletedBytes = 0;

                int expiryFloor = acknowledgedQts;
                foreach (TLDto.TLSecretChatQtsEntry entry in entries)
                {
                    int entryQts = entry.AsSecretChatQtsEntry().Qts;
                    if (entryQts <= expiryFloor)
                    {
                        continue;
                    }
                    if (entryQts != expiryFloor + 1 ||
                        entry.AsSecretChatQtsEntry().ExpiresAt > now)
                    {
                        break;
                    }
                    expiryFloor = entryQts;
                    deletedQts.Add(entryQts);
                    deletedBytes +=
                        entry.AsSecretChatQtsEntry().EncryptedMessage.Length;
                }

                int implicitAcknowledgement = Math.Max(expiryFloor,
                    Math.Min(requestQts, lastPersistedQts));
                foreach (TLDto.TLSecretChatQtsEntry entry in entries)
                {
                    int entryQts = entry.AsSecretChatQtsEntry().Qts;
                    if (entryQts <= implicitAcknowledgement && deletedQts.Add(entryQts))
                    {
                        deletedBytes +=
                            entry.AsSecretChatQtsEntry().EncryptedMessage.Length;
                    }
                }
                foreach (int qts in deletedQts)
                {
                    await _qtsEntries.DeleteAsync(recipientAuthKeyId, qts);
                }

                TLDto.TLSecretChatQtsState snapshotState = state;
                if (implicitAcknowledgement != acknowledgedQts || deletedQts.Count > 0)
                {
                    snapshotState = BuildQtsState(recipientAuthKeyId, lastPersistedQts,
                        implicitAcknowledgement,
                        Math.Max(0, queuedEvents - deletedQts.Count),
                        Math.Max(0, queuedBytes - deletedBytes), null);
                    try
                    {
                        PutQtsState(snapshotState);
                        await FlushAsync("secret-chat qts difference acknowledgement");
                    }
                    catch
                    {
                        snapshotState.Dispose();
                        throw;
                    }
                    state.Dispose();
                    state = snapshotState;
                }

                int selectionFloor = Math.Max(requestQts, implicitAcknowledgement);
                TLDto.TLSecretChatQtsEntry[] remaining = entries
                    .Where(x => !deletedQts.Contains(x.AsSecretChatQtsEntry().Qts) &&
                                x.AsSecretChatQtsEntry().Qts > selectionFloor)
                    .OrderBy(x => x.AsSecretChatQtsEntry().Qts)
                    .ToArray();
                return new SecretChatQtsDifferenceResult(
                    remaining.Take(limit).ToArray(), snapshotState, lastPersistedQts,
                    remaining.Length > limit);
            }
            catch
            {
                state.Dispose();
                throw;
            }
        }
        finally
        {
            gate.Release();
        }
    }

    public async ValueTask<IReadOnlyList<long>> GetQtsMaintenanceAuthKeyIdsAsync(
        long afterAuthKeyId, int limit,
        CancellationToken cancellationToken = default)
    {
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(limit);
        var authKeyIds = new HashSet<long>();
        await foreach (byte[] bytes in _qtsStates.IterateAsync()
                           .WithCancellation(cancellationToken))
        {
            using TLDto.TLSecretChatQtsState state = ReadQtsState(bytes);
            long authKeyId = state.AsSecretChatQtsState().AuthKeyId;
            if (authKeyId > afterAuthKeyId)
            {
                authKeyIds.Add(authKeyId);
            }
        }
        return authKeyIds.Order().Take(limit).ToArray();
    }

    public async ValueTask<SecretChatQtsMaintenanceResult> MaintainQtsAsync(
        long recipientAuthKeyId, int now,
        Func<ValueTask<int>> getCurrentQts, Func<ValueTask<int>> incrementQts,
        CancellationToken cancellationToken = default)
    {
        SemaphoreSlim gate = GetGate(_authKeyGates, recipientAuthKeyId);
        await gate.WaitAsync(cancellationToken);
        try
        {
            TLDto.TLSecretChatQtsState state = await GetOrCreateQtsStateAsync(
                recipientAuthKeyId, cancellationToken);
            try
            {
                TLDto.SecretChatQtsState before = state.AsSecretChatQtsState();
                bool recoveredPending = before.Flags[0] || before.Flags[1];
                state = await RecoverPendingQtsAppendAsync(state, getCurrentQts,
                    incrementQts);
                ExpiredQtsResult expiry = await ExpireContiguousQtsAsync(state,
                    recipientAuthKeyId, now, cancellationToken);
                state = expiry.State;
                TLDto.SecretChatQtsState row = state.AsSecretChatQtsState();
                return new SecretChatQtsMaintenanceResult(recipientAuthKeyId,
                    recoveredPending, expiry.Events, expiry.Bytes,
                    row.AcknowledgedQts, row.QueuedEvents, row.QueuedBytes);
            }
            finally
            {
                state.Dispose();
            }
        }
        finally
        {
            gate.Release();
        }
    }

    public async ValueTask<SecretChatRetentionCleanupResult> CleanupRetentionAsync(
        int now, int receiptRetentionSeconds, int maxItems,
        CancellationToken cancellationToken = default)
    {
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(receiptRetentionSeconds);
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxItems);
        int receiptCutoff = checked(now - receiptRetentionSeconds);
        var expiredReceipts = new List<(int ChatId, long SenderAuthKeyId,
            long RandomId)>();
        int scannedReceipts = 0;
        await foreach (byte[] bytes in _sendReceipts.IterateAsync()
                           .WithCancellation(cancellationToken))
        {
            if (scannedReceipts >= maxItems)
            {
                break;
            }
            scannedReceipts++;
            using TLDto.TLSecretChatSendReceipt receipt = ReadSendReceipt(bytes);
            TLDto.SecretChatSendReceipt row = receipt.AsSecretChatSendReceipt();
            if (row.Date <= receiptCutoff)
            {
                expiredReceipts.Add((row.ChatId, row.SenderAuthKeyId,
                    row.RandomId));
            }
        }

        int deletedReceipts = 0;
        foreach (var key in expiredReceipts)
        {
            SemaphoreSlim chatGate = GetGate(_chatGates, key.ChatId);
            await chatGate.WaitAsync(cancellationToken);
            try
            {
                byte[]? currentBytes = await _sendReceipts.GetAsync(key.ChatId,
                    key.SenderAuthKeyId, key.RandomId);
                if (currentBytes is not { Length: > 0 })
                {
                    continue;
                }
                using TLDto.TLSecretChatSendReceipt current =
                    ReadSendReceipt(currentBytes);
                if (current.AsSecretChatSendReceipt().Date <= receiptCutoff &&
                    await _sendReceipts.DeleteAsync(key.ChatId,
                        key.SenderAuthKeyId, key.RandomId))
                {
                    deletedReceipts++;
                }
            }
            finally
            {
                chatGate.Release();
            }
        }

        var expiredControls = new List<(long AuthKeyId, long UpdateId)>();
        int scannedControls = 0;
        await foreach (byte[] bytes in _controlUpdates.IterateAsync()
                           .WithCancellation(cancellationToken))
        {
            if (scannedControls >= maxItems)
            {
                break;
            }
            scannedControls++;
            using TLDto.TLSecretChatControlUpdate control =
                ReadControlUpdate(bytes);
            TLDto.SecretChatControlUpdate row = control.AsSecretChatControlUpdate();
            if (row.ExpiresAt <= now)
            {
                expiredControls.Add((row.RecipientAuthKeyId, row.UpdateId));
            }
        }

        int deletedControls = 0;
        foreach (var key in expiredControls)
        {
            SemaphoreSlim authGate = GetGate(_authKeyGates, key.AuthKeyId);
            await authGate.WaitAsync(cancellationToken);
            try
            {
                byte[]? currentBytes = await _controlUpdates.GetAsync(key.AuthKeyId,
                    key.UpdateId);
                if (currentBytes is not { Length: > 0 })
                {
                    continue;
                }
                using TLDto.TLSecretChatControlUpdate current =
                    ReadControlUpdate(currentBytes);
                if (current.AsSecretChatControlUpdate().ExpiresAt <= now &&
                    await _controlUpdates.DeleteAsync(key.AuthKeyId, key.UpdateId))
                {
                    deletedControls++;
                }
            }
            finally
            {
                authGate.Release();
            }
        }

        if (deletedReceipts != 0 || deletedControls != 0)
        {
            await FlushAsync("secret-chat retention cleanup");
        }
        return new SecretChatRetentionCleanupResult(scannedReceipts,
            deletedReceipts, scannedControls, deletedControls);
    }

    public async ValueTask<bool> PutControlUpdateAsync(
        TLDto.TLSecretChatControlUpdate update,
        CancellationToken cancellationToken = default)
    {
        long recipientAuthKeyId = update.AsSecretChatControlUpdate().RecipientAuthKeyId;
        long updateId = update.AsSecretChatControlUpdate().UpdateId;
        SemaphoreSlim gate = GetGate(_authKeyGates, recipientAuthKeyId);
        await gate.WaitAsync(cancellationToken);
        try
        {
            if (await IsRevokedAsync(recipientAuthKeyId))
            {
                return false;
            }
            byte[]? existing = await _controlUpdates.GetAsync(recipientAuthKeyId,
                updateId);
            if (existing is { Length: > 0 })
            {
                return ControlUpdatesEqual(ReadControlUpdate(existing), update);
            }
            bool result = _controlUpdates.Put(update.AsSpan().ToArray(),
                recipientAuthKeyId, updateId);
            await FlushAsync("secret-chat control update");
            return result;
        }
        finally
        {
            gate.Release();
        }
    }

    public async ValueTask<IReadOnlyList<TLDto.TLSecretChatControlUpdate>>
        GetControlUpdatesAsync(long recipientAuthKeyId,
            CancellationToken cancellationToken = default)
    {
        List<TLDto.TLSecretChatControlUpdate> updates = new();
        await foreach (byte[] bytes in _controlUpdates.IterateAsync(recipientAuthKeyId)
                           .WithCancellation(cancellationToken))
        {
            updates.Add(ReadControlUpdate(bytes));
        }
        return updates.OrderBy(x => x.AsSecretChatControlUpdate().Date)
            .ThenBy(x => x.AsSecretChatControlUpdate().UpdateId).ToArray();
    }

    public async ValueTask<bool> DeleteControlUpdatesAsync(long recipientAuthKeyId,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        bool result = await _controlUpdates.DeleteAsync(recipientAuthKeyId);
        await FlushAsync("secret-chat control-update deletion");
        return result;
    }

    public async ValueTask<bool> DeleteControlUpdateAsync(long recipientAuthKeyId,
        long updateId, CancellationToken cancellationToken = default)
    {
        SemaphoreSlim gate = GetGate(_authKeyGates, recipientAuthKeyId);
        await gate.WaitAsync(cancellationToken);
        try
        {
            bool result = await _controlUpdates.DeleteAsync(recipientAuthKeyId,
                updateId);
            await FlushAsync("secret-chat control-update deletion");
            return result;
        }
        finally
        {
            gate.Release();
        }
    }

    public async ValueTask<SecretChatControlDifferenceResult>
        GetControlDifferenceAsync(long recipientAuthKeyId, int requestDate,
            int responseDate, int now, bool isProbe,
            CancellationToken cancellationToken = default)
    {
        if (isProbe)
        {
            return new SecretChatControlDifferenceResult([]);
        }

        SemaphoreSlim gate = GetGate(_authKeyGates, recipientAuthKeyId);
        await gate.WaitAsync(cancellationToken);
        try
        {
            var offered = new List<TLDto.TLSecretChatControlUpdate>();
            try
            {
                bool changed = false;
                await foreach (byte[] bytes in _controlUpdates.IterateAsync(
                                   recipientAuthKeyId)
                                   .WithCancellation(cancellationToken))
                {
                    TLDto.TLSecretChatControlUpdate update = ReadControlUpdate(bytes);
                    long updateId = update.AsSecretChatControlUpdate().UpdateId;
                    bool confirmed = update.AsSecretChatControlUpdate().Flags[0] &&
                                     update.AsSecretChatControlUpdate()
                                         .OfferedStateDate <= requestDate;
                    if (update.AsSecretChatControlUpdate().ExpiresAt <= now ||
                        confirmed)
                    {
                        await _controlUpdates.DeleteAsync(recipientAuthKeyId,
                            updateId);
                        changed = true;
                        continue;
                    }

                    TLDto.TLSecretChatControlUpdate next = BuildControlUpdate(update,
                        responseDate);
                    offered.Add(next);
                    _controlUpdates.Put(next.AsSpan().ToArray(), recipientAuthKeyId,
                        next.AsSecretChatControlUpdate().UpdateId);
                    changed = true;
                }
                if (changed)
                {
                    await FlushAsync("secret-chat control difference");
                }
                return new SecretChatControlDifferenceResult(offered
                    .OrderBy(x => x.AsSecretChatControlUpdate().Date)
                    .ThenBy(x => x.AsSecretChatControlUpdate().UpdateId)
                    .ToArray());
            }
            catch
            {
                foreach (TLDto.TLSecretChatControlUpdate update in offered)
                {
                    update.Dispose();
                }
                throw;
            }
        }
        finally
        {
            gate.Release();
        }
    }

    public async ValueTask<bool> PutEncryptedFileAsync(
        TLDto.TLSecretChatEncryptedFile file,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        long fileId = file.AsSecretChatEncryptedFile().FileId;
        long accessHash = file.AsSecretChatEncryptedFile().AccessHash;
        long uploadFileId = file.AsSecretChatEncryptedFile().UploadFileId;
        bool result = _encryptedFiles.Put(file.AsSpan().ToArray(), fileId,
            accessHash, uploadFileId);
        await FlushAsync("secret-chat encrypted-file metadata");
        return result;
    }

    public async ValueTask<TLDto.TLSecretChatEncryptedFile?> GetEncryptedFileAsync(
        long fileId,
        long accessHash, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        byte[]? bytes = await _encryptedFiles.GetBySecondaryIndexAsync(
            "by_exact", fileId, accessHash);
        return bytes is { Length: > 0 }
            ? ReadEncryptedFile(bytes)
            : null;
    }

    public async ValueTask<TLDto.TLSecretChatEncryptedFile?>
        GetEncryptedFileByIdAsync(long fileId,
            CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        byte[]? bytes = await _encryptedFiles.GetBySecondaryIndexAsync(
            "by_id", fileId);
        return bytes is { Length: > 0 }
            ? ReadEncryptedFile(bytes)
            : null;
    }

    public async ValueTask<TLDto.TLSecretChatEncryptedFile?>
        GetEncryptedFileByUploadIdAsync(long uploadFileId,
            CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        byte[]? bytes = await _encryptedFiles.GetBySecondaryIndexAsync(
            "by_upload", uploadFileId);
        return bytes is { Length: > 0 }
            ? ReadEncryptedFile(bytes)
            : null;
    }

    public async ValueTask<SecretChatFileAssociationStatus>
        TryAssociateEncryptedFileAsync(
            TLDto.TLSecretChatEncryptedFileAssociation association,
            int maxAssociations, CancellationToken cancellationToken = default)
    {
        long fileId = association.AsSecretChatEncryptedFileAssociation().FileId;
        int chatId = association.AsSecretChatEncryptedFileAssociation().ChatId;
        SemaphoreSlim gate = GetGate(_fileGates, fileId);
        await gate.WaitAsync(cancellationToken);
        try
        {
            byte[]? existing = await _encryptedFileAssociations.GetAsync(
                fileId, chatId);
            if (existing is { Length: > 0 })
            {
                return SecretChatFileAssociationStatus.AlreadyExists;
            }
            IReadOnlyList<TLDto.TLSecretChatEncryptedFileAssociation> associations =
                await GetEncryptedFileAssociationsAsync(fileId,
                    cancellationToken);
            if (associations.Count >= maxAssociations)
            {
                return SecretChatFileAssociationStatus.LimitExceeded;
            }
            _encryptedFileAssociations.Put(association.AsSpan().ToArray(), fileId,
                chatId);
            await FlushAsync("secret-chat encrypted-file association");
            return SecretChatFileAssociationStatus.Created;
        }
        finally
        {
            gate.Release();
        }
    }

    public async ValueTask<IReadOnlyList<TLDto.TLSecretChatEncryptedFileAssociation>>
        GetEncryptedFileAssociationsAsync(long fileId,
            CancellationToken cancellationToken = default)
    {
        List<TLDto.TLSecretChatEncryptedFileAssociation> associations = new();
        await foreach (byte[] bytes in _encryptedFileAssociations.IterateAsync(fileId)
                           .WithCancellation(cancellationToken))
        {
            associations.Add(ReadEncryptedFileAssociation(bytes));
        }
        return associations.OrderBy(x =>
            x.AsSecretChatEncryptedFileAssociation().ChatId).ToArray();
    }

    public async ValueTask<IReadOnlyList<TLDto.TLSecretChatEncryptedFileAssociation>>
        GetEncryptedFilesByChatAsync(int chatId,
            CancellationToken cancellationToken = default)
    {
        List<TLDto.TLSecretChatEncryptedFileAssociation> associations = new();
        await foreach (byte[] bytes in _encryptedFileAssociations
                           .IterateBySecondaryIndexAsync("by_chat", chatId)
                           .WithCancellation(cancellationToken))
        {
            associations.Add(ReadEncryptedFileAssociation(bytes));
        }
        return associations.OrderBy(x =>
            x.AsSecretChatEncryptedFileAssociation().FileId).ToArray();
    }

    public async ValueTask<SecretChatAuthKeyRevocationResult> RevokeAuthKeyAsync(
        long authKeyId, int date, CancellationToken cancellationToken = default)
    {
        SemaphoreSlim authGate = GetGate(_authKeyGates, authKeyId);
        await authGate.WaitAsync(cancellationToken);
        try
        {
            TLDto.TLSecretChatAuthKeyRevocation? previousRevocation =
                await GetRevocationAsync(authKeyId);
            if (previousRevocation is not null)
            {
                TLDto.TLSecretChatAuthKeyRevocation stored = previousRevocation.Value;
                return new SecretChatAuthKeyRevocationResult(true,
                    stored.AsSecretChatAuthKeyRevocation().Date,
                    stored.AsSecretChatAuthKeyRevocation().NotificationsCompleted
                        ? []
                        : AffectedPeers(stored));
            }

            Dictionary<int, TLDto.TLSecretChatState> affectedChats = new();
            foreach (TLDto.TLSecretChatState chat in
                     await GetChatsByAuthKeyAsync(authKeyId,
                         cancellationToken))
            {
                affectedChats[ChatId(chat)] = chat;
            }
            await foreach (byte[] bytes in _requestedDevices.IterateAsync(authKeyId)
                               .WithCancellation(cancellationToken))
            {
                TLDto.TLSecretChatRequestedDevice target = ReadRequestedDevice(bytes);
                int targetChatId = target.AsSecretChatRequestedDevice().ChatId;
                TLDto.TLSecretChatState? chat = await GetChatInternalAsync(targetChatId,
                    cancellationToken);
                if (chat is not null)
                {
                    affectedChats[ChatId(chat.Value)] = chat.Value;
                }
            }

            List<TLDto.TLSecretChatRevokedPeer> peers = new();
            List<TLDto.TLSecretChatRevokedPeer> uniquePeers = new();
            try
            {
                foreach (int chatId in affectedChats.Keys.Order())
                {
                    SemaphoreSlim chatGate = GetGate(_chatGates, chatId);
                    await chatGate.WaitAsync(cancellationToken);
                    try
                    {
                        TLDto.TLSecretChatState? chat = await GetChatInternalAsync(
                            chatId, cancellationToken);
                        if (chat is null ||
                            State(chat.Value) == SecretChatPersistenceState.Discarded)
                        {
                            continue;
                        }
                        TLDto.TLSecretChatState existingChat = chat.Value;
                        SecretChatPersistenceState state = State(existingChat);
                        long initiatorKey = InitiatorAuthKeyId(existingChat);
                        long? recipientKey = RecipientAuthKeyId(existingChat);

                        if (initiatorKey == authKeyId)
                        {
                            await DeleteControlTargetsByChatAsync(chatId,
                                cancellationToken);
                            if (state == SecretChatPersistenceState.Active &&
                                recipientKey is long activeRecipientKey)
                            {
                                peers.Add(BuildRevokedPeer(chatId, activeRecipientKey,
                                    RecipientUserId(existingChat), 0));
                            }
                            else
                            {
                                foreach (long requestedKey in
                                         RequestedRecipientAuthKeyIds(existingChat)
                                             .Distinct())
                                {
                                    peers.Add(BuildRevokedPeer(chatId, requestedKey,
                                        RecipientUserId(existingChat), 0));
                                }
                            }
                            using TLDto.TLSecretChatState discarded =
                                BuildDiscardedChat(existingChat, false, date);
                            ReplaceChat(existingChat, discarded);
                            DeleteRequestedDevices(existingChat);
                            continue;
                        }

                        if (state == SecretChatPersistenceState.Active &&
                            recipientKey == authKeyId)
                        {
                            await DeleteControlTargetsByChatAsync(chatId,
                                cancellationToken);
                            peers.Add(BuildRevokedPeer(chatId, initiatorKey,
                                InitiatorUserId(existingChat), 0));
                            using TLDto.TLSecretChatState discarded =
                                BuildDiscardedChat(existingChat, false, date);
                            ReplaceChat(existingChat, discarded);
                            continue;
                        }

                        long[] requestedKeys =
                            RequestedRecipientAuthKeyIds(existingChat);
                        if (state == SecretChatPersistenceState.Pending &&
                            requestedKeys.Contains(authKeyId))
                        {
                            _controlTargets.Delete(authKeyId, chatId);
                            long[] remaining = requestedKeys
                                .Where(x => x != authKeyId).Distinct().ToArray();
                            _requestedDevices.Delete(authKeyId, chatId);
                            if (remaining.Length == 0)
                            {
                                await DeleteControlTargetsByChatAsync(chatId,
                                    cancellationToken);
                                peers.Add(BuildRevokedPeer(chatId, initiatorKey,
                                    InitiatorUserId(existingChat), 0));
                                using TLDto.TLSecretChatState discarded =
                                    BuildDiscardedChat(existingChat, false, date);
                                ReplaceChat(existingChat, discarded);
                            }
                            else
                            {
                                using TLDto.TLSecretChatState detached =
                                    BuildPendingWithRequestedKeys(existingChat, date,
                                        remaining);
                                ReplaceChat(existingChat, detached);
                            }
                        }
                    }
                    finally
                    {
                        chatGate.Release();
                    }
                }

                await _qtsEntries.DeleteAsync(authKeyId);
                await _qtsStates.DeleteAsync(authKeyId);
                await _controlUpdates.DeleteAsync(authKeyId);
                await _controlTargets.DeleteAsync(authKeyId);
                await _requestedDevices.DeleteAsync(authKeyId);
                foreach (TLDto.TLSecretChatRevokedPeer peer in peers.DistinctBy(
                             x => (x.AsSecretChatRevokedPeer().ChatId,
                                 x.AsSecretChatRevokedPeer().PeerAuthKeyId)))
                {
                    TLDto.SecretChatRevokedPeer row = peer.AsSecretChatRevokedPeer();
                    uniquePeers.Add(BuildRevokedPeer(row.ChatId, row.PeerAuthKeyId,
                        row.PeerUserId, CreateControlUpdateId()));
                }
                using TLDto.TLSecretChatAuthKeyRevocation tombstone =
                    BuildRevocation(authKeyId, date, false, uniquePeers);
                _revocations.Put(tombstone.AsSpan().ToArray(), authKeyId);
                await FlushAsync("secret-chat auth-key revocation");
                return new SecretChatAuthKeyRevocationResult(false, date,
                    uniquePeers);
            }
            catch
            {
                foreach (TLDto.TLSecretChatRevokedPeer peer in uniquePeers)
                {
                    peer.Dispose();
                }
                throw;
            }
            finally
            {
                foreach (TLDto.TLSecretChatRevokedPeer peer in peers)
                {
                    peer.Dispose();
                }
            }
        }
        finally
        {
            authGate.Release();
        }
    }

    public async ValueTask<bool> CompleteAuthKeyRevocationAsync(long authKeyId,
        CancellationToken cancellationToken = default)
    {
        SemaphoreSlim gate = GetGate(_authKeyGates, authKeyId);
        await gate.WaitAsync(cancellationToken);
        try
        {
            TLDto.TLSecretChatAuthKeyRevocation? tombstone =
                await GetRevocationAsync(authKeyId);
            if (tombstone is null)
            {
                return false;
            }
            if (!tombstone.Value.AsSecretChatAuthKeyRevocation().NotificationsCompleted)
            {
                using TLDto.TLSecretChatAuthKeyRevocation completed = BuildRevocation(
                    authKeyId, tombstone.Value.AsSecretChatAuthKeyRevocation().Date,
                    true, AffectedPeers(tombstone.Value));
                _revocations.Put(completed.AsSpan().ToArray(), authKeyId);
                await FlushAsync("secret-chat auth-key revocation completion");
            }
            return true;
        }
        finally
        {
            gate.Release();
        }
    }

    private async ValueTask<TLDto.TLSecretChatState?> GetChatInternalAsync(int chatId,
        CancellationToken cancellationToken)
    {
        await foreach (byte[] bytes in _chats.IterateAsync(chatId)
                           .WithCancellation(cancellationToken))
        {
            return ReadChat(bytes);
        }
        return null;
    }

    private async ValueTask<TLDto.TLSecretChatState?>
        GetChatByInitiatorRandomIdInternalAsync(long initiatorUserId, int randomId,
            CancellationToken cancellationToken)
    {
        await foreach (byte[] bytes in _chats.IterateBySecondaryIndexAsync(
                           "by_initiator_random_id", initiatorUserId, randomId)
                           .WithCancellation(cancellationToken))
        {
            return ReadChat(bytes);
        }
        return null;
    }

    private async ValueTask<TLDto.TLSecretChatPendingPair?> GetPendingPairAsync(
        long initiatorUserId, long recipientUserId)
    {
        byte[]? bytes = await _pendingPairs.GetAsync(initiatorUserId,
            recipientUserId);
        return bytes is { Length: > 0 }
            ? new TLDto.TLSecretChatPendingPair(bytes, 0, bytes.Length)
            : null;
    }

    private async ValueTask<TLDto.TLSecretChatState?> GetPendingChatForPairAsync(
        long initiatorUserId, long recipientUserId,
        CancellationToken cancellationToken)
    {
        await foreach (byte[] bytes in _chats.IterateBySecondaryIndexAsync(
                           "by_initiator_user", initiatorUserId)
                           .WithCancellation(cancellationToken))
        {
            TLDto.TLSecretChatState chat = ReadChat(bytes);
            if (RecipientUserId(chat) == recipientUserId &&
                State(chat) == SecretChatPersistenceState.Pending)
            {
                return chat;
            }
            chat.Dispose();
        }
        return null;
    }

    private async ValueTask<int> CountChatsByStateAsync(long authKeyId,
        SecretChatPersistenceState state, CancellationToken cancellationToken)
    {
        IReadOnlyList<TLDto.TLSecretChatState> chats = await GetChatsByAuthKeyAsync(
            authKeyId, cancellationToken);
        int count = 0;
        foreach (TLDto.TLSecretChatState chat in chats)
        {
            using (chat)
            {
                if (State(chat) == state)
                {
                    count++;
                }
            }
        }
        return count;
    }

    private async ValueTask<int> CountRequestedChatsAsync(long authKeyId,
        CancellationToken cancellationToken)
    {
        int count = 0;
        await foreach (byte[] _ in _requestedDevices.IterateAsync(authKeyId)
                           .WithCancellation(cancellationToken))
        {
            count++;
        }
        return count;
    }

    private async ValueTask<TLDto.TLSecretChatQtsState> GetOrCreateQtsStateAsync(
        long authKeyId,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        byte[]? bytes = await _qtsStates.GetAsync(authKeyId);
        if (bytes is { Length: > 0 })
        {
            return ReadQtsState(bytes);
        }
        TLDto.TLSecretChatQtsState seed = BuildQtsState(authKeyId, 1, 1, 0, 0,
            null);
        try
        {
            PutQtsState(seed);
        }
        catch
        {
            seed.Dispose();
            throw;
        }
        return seed;
    }

    private async ValueTask<TLDto.TLSecretChatQtsState> RecoverPendingQtsAppendAsync(
        TLDto.TLSecretChatQtsState state, Func<ValueTask<int>> getCurrentQts,
        Func<ValueTask<int>> incrementQts)
    {
        TLDto.TLSecretChatQtsPending? pending = GetPendingAppend(state);
        TLDto.TLSecretChatSendQtsPending? pendingSend = GetPendingSend(state);
        if (pending is null && pendingSend is null)
        {
            return state;
        }

        long authKeyId = state.AsSecretChatQtsState().AuthKeyId;
        int lastPersistedQts = state.AsSecretChatQtsState().LastPersistedQts;
        int acknowledgedQts = state.AsSecretChatQtsState().AcknowledgedQts;
        int queuedEvents = state.AsSecretChatQtsState().QueuedEvents;
        long queuedBytes = state.AsSecretChatQtsState().QueuedBytes;
        int chatId;
        long randomId;
        int date;
        int expiresAt;
        byte[] encryptedMessage;
        long? senderAuthKeyId = null;
        byte[]? result = null;
        if (pendingSend is not null)
        {
            TLDto.SecretChatSendQtsPending row =
                pendingSend.Value.AsSecretChatSendQtsPending();
            senderAuthKeyId = row.SenderAuthKeyId;
            chatId = row.ChatId;
            randomId = row.RandomId;
            date = row.Date;
            expiresAt = row.ExpiresAt;
            encryptedMessage = row.EncryptedMessage.ToArray();
            result = row.Result.ToArray();
        }
        else
        {
            TLDto.SecretChatQtsPending row = pending!.Value.AsSecretChatQtsPending();
            chatId = row.ChatId;
            randomId = row.RandomId;
            date = row.Date;
            expiresAt = row.ExpiresAt;
            encryptedMessage = row.EncryptedMessage.ToArray();
        }

        int? recoveredQts = null;
        IReadOnlyList<TLDto.TLSecretChatQtsEntry> existingEntries =
            await ReadQtsEntriesInternalAsync(authKeyId, CancellationToken.None);
        foreach (TLDto.TLSecretChatQtsEntry existingEntry in existingEntries)
        {
            using (existingEntry)
            {
                TLDto.SecretChatQtsEntry row = existingEntry.AsSecretChatQtsEntry();
                if (row.ChatId == chatId && row.RandomId == randomId &&
                    row.Date == date && row.EncryptedMessage.SequenceEqual(
                        encryptedMessage))
                {
                    recoveredQts = row.Qts;
                }
            }
        }

        int receiptDate = date;
        if (senderAuthKeyId is long sender)
        {
            TLDto.TLSecretChatSendReceipt? existingReceipt =
                await GetSendReceiptAsync(chatId, sender, randomId);
            if (existingReceipt is not null)
            {
                using TLDto.TLSecretChatSendReceipt receipt = existingReceipt.Value;
                TLDto.SecretChatSendReceipt receiptRow =
                    receipt.AsSecretChatSendReceipt();
                recoveredQts ??= receiptRow.RecipientQts;
                receiptDate = receiptRow.Date;
                result = receiptRow.Result.ToArray();
            }
        }

        int qts;
        if (recoveredQts is int existingQts && existingQts > lastPersistedQts)
        {
            qts = existingQts;
        }
        else
        {
            int currentQts = await getCurrentQts();
            qts = currentQts > lastPersistedQts
                ? currentQts
                : await incrementQts();
        }
        if (qts <= lastPersistedQts)
        {
            throw new InvalidOperationException(
                "The canonical qts counter did not advance during recovery.");
        }

        using TLDto.TLSecretChatQtsEntry entry = BuildQtsEntry(authKeyId, qts,
            chatId, randomId, date, expiresAt, encryptedMessage);
        TLDto.TLSecretChatQtsState recovered = BuildQtsState(authKeyId, qts,
            acknowledgedQts, checked(queuedEvents + 1),
            checked(queuedBytes + encryptedMessage.LongLength), null, null);
        try
        {
            _qtsEntries.Put(entry.AsSpan().ToArray(), authKeyId, qts);
            if (senderAuthKeyId is long senderKey)
            {
                using TLDto.TLSecretChatSendReceipt receipt = BuildSendReceipt(chatId,
                    senderKey, randomId, receiptDate, qts, result!);
                _sendReceipts.Put(receipt.AsSpan().ToArray(), chatId, senderKey,
                    randomId);
            }
            PutQtsState(recovered);
            await FlushAsync("secret-chat qts reservation recovery");
        }
        catch
        {
            recovered.Dispose();
            throw;
        }
        state.Dispose();
        return recovered;
    }

    private async ValueTask<IReadOnlyList<TLDto.TLSecretChatQtsEntry>>
        ReadQtsEntriesInternalAsync(long recipientAuthKeyId,
            CancellationToken cancellationToken)
    {
        var entries = new List<TLDto.TLSecretChatQtsEntry>();
        await foreach (byte[] bytes in _qtsEntries.IterateAsync(recipientAuthKeyId)
                           .WithCancellation(cancellationToken))
        {
            entries.Add(ReadQtsEntry(bytes));
        }
        return entries.OrderBy(x => x.AsSecretChatQtsEntry().Qts).ToArray();
    }

    private async ValueTask<ExpiredQtsResult> ExpireContiguousQtsAsync(
        TLDto.TLSecretChatQtsState state, long recipientAuthKeyId, int now,
        CancellationToken cancellationToken)
    {
        int lastPersistedQts = state.AsSecretChatQtsState().LastPersistedQts;
        int currentAcknowledgedQts = state.AsSecretChatQtsState().AcknowledgedQts;
        int queuedEvents = state.AsSecretChatQtsState().QueuedEvents;
        long queuedBytes = state.AsSecretChatQtsState().QueuedBytes;
        TLDto.TLSecretChatQtsPending? pending = GetPendingAppend(state);
        TLDto.TLSecretChatSendQtsPending? pendingSend = GetPendingSend(state);
        IReadOnlyList<TLDto.TLSecretChatQtsEntry> entries =
            await ReadQtsEntriesInternalAsync(recipientAuthKeyId,
                cancellationToken);
        var expired = new List<TLDto.TLSecretChatQtsEntry>();
        int nextQts = currentAcknowledgedQts + 1;
        try
        {
            foreach (TLDto.TLSecretChatQtsEntry entry in entries)
            {
                TLDto.SecretChatQtsEntry entryRow =
                    entry.AsSecretChatQtsEntry();
                if (entryRow.Qts <= currentAcknowledgedQts)
                {
                    continue;
                }
                if (entryRow.Qts != nextQts || entryRow.ExpiresAt > now)
                {
                    break;
                }
                expired.Add(entry);
                nextQts++;
            }

            if (expired.Count == 0)
            {
                return new ExpiredQtsResult(state, 0, 0);
            }

            int expectedEvents = expired.Count;
            long expectedBytes = 0;
            foreach (TLDto.TLSecretChatQtsEntry entry in expired)
            {
                expectedBytes = checked(expectedBytes + entry
                    .AsSecretChatQtsEntry().EncryptedMessage.Length);
            }
            int acknowledgedQts = nextQts - 1;
            int remainingEvents = checked(queuedEvents - expectedEvents);
            long remainingBytes = checked(queuedBytes - expectedBytes);
            if (remainingEvents < 0 || remainingBytes < 0)
            {
                throw new InvalidDataException(
                    "Secret-chat qts counters are inconsistent with durable entries.");
            }
            (int events, long bytes) = await DeleteQtsEntriesAsync(
                recipientAuthKeyId, expired);
            TLDto.TLSecretChatQtsState next = BuildQtsState(recipientAuthKeyId,
                lastPersistedQts, acknowledgedQts,
                remainingEvents, remainingBytes, pending, pendingSend);
            try
            {
                PutQtsState(next);
                await FlushAsync("secret-chat qts expiry");
            }
            catch
            {
                next.Dispose();
                throw;
            }
            state.Dispose();
            return new ExpiredQtsResult(next, events, bytes);
        }
        finally
        {
            foreach (TLDto.TLSecretChatQtsEntry entry in entries)
            {
                entry.Dispose();
            }
        }
    }

    private async ValueTask<(int Events, long Bytes)> DeleteQtsEntriesAsync(
        long recipientAuthKeyId,
        IEnumerable<TLDto.TLSecretChatQtsEntry> entries)
    {
        int deletedEvents = 0;
        long deletedBytes = 0;
        foreach (TLDto.TLSecretChatQtsEntry entry in entries)
        {
            int qts = entry.AsSecretChatQtsEntry().Qts;
            deletedBytes += entry.AsSecretChatQtsEntry().EncryptedMessage.Length;
            await _qtsEntries.DeleteAsync(recipientAuthKeyId, qts);
            deletedEvents++;
        }
        return (deletedEvents, deletedBytes);
    }

    private void PutQtsState(TLDto.TLSecretChatQtsState state) =>
        _qtsStates.Put(state.AsSpan().ToArray(),
            state.AsSecretChatQtsState().AuthKeyId);

    private readonly record struct ExpiredQtsResult(
        TLDto.TLSecretChatQtsState State, int Events, long Bytes);

    private async ValueTask<bool> IsRevokedAsync(long authKeyId)
    {
        return await GetRevocationAsync(authKeyId) is not null;
    }

    private async ValueTask<TLDto.TLSecretChatAuthKeyRevocation?> GetRevocationAsync(
        long authKeyId)
    {
        byte[]? bytes = await _revocations.GetAsync(authKeyId);
        return bytes is { Length: > 0 }
            ? ReadRevocation(bytes)
            : null;
    }

    private void ReplaceChat(TLDto.TLSecretChatState previous,
        TLDto.TLSecretChatState next)
    {
        DeleteChat(previous);
        PutChat(next);
        if (State(previous) == SecretChatPersistenceState.Pending &&
            State(next) != SecretChatPersistenceState.Pending)
        {
            _pendingPairs.Delete(InitiatorUserId(previous),
                RecipientUserId(previous));
        }
    }

    private void PutChat(TLDto.TLSecretChatState chat)
    {
        _chats.Put(chat.AsSpan().ToArray(), ChatId(chat), InitiatorAuthKeyId(chat),
            RecipientAuthKeyId(chat) ?? 0L, InitiatorUserId(chat), ChatId(chat));
    }

    private void DeleteChat(TLDto.TLSecretChatState chat)
    {
        _chats.Delete(ChatId(chat), InitiatorAuthKeyId(chat),
            RecipientAuthKeyId(chat) ?? 0L, InitiatorUserId(chat), ChatId(chat));
    }

    private void DeleteRequestedDevices(TLDto.TLSecretChatState chat)
    {
        foreach (long authKeyId in RequestedRecipientAuthKeyIds(chat).Distinct())
        {
            _requestedDevices.Delete(authKeyId, ChatId(chat));
        }
    }

    private async ValueTask DeleteControlTargetsByChatAsync(int chatId,
        CancellationToken cancellationToken)
    {
        long[] authKeyIds = (await GetControlTargetAuthKeyIdsAsync(chatId,
            cancellationToken)).ToArray();
        foreach (long authKeyId in authKeyIds)
        {
            _controlTargets.Delete(authKeyId, chatId);
        }
    }

    private static TLDto.TLSecretChatState BuildPendingChat(
        TLDto.TLSecretChatState source, IReadOnlyList<long> requestedAuthKeys)
    {
        TLDto.SecretChatState row = source.AsSecretChatState();
        return BuildChat(row.HistoryDeleted, null, null, null, row.ChatId,
            row.AccessHash, row.InitiatorUserId, row.RecipientUserId,
            row.InitiatorAuthKeyId, SecretChatPersistenceState.Pending, row.CreatedAt,
            row.UpdatedAt, row.GA.ToArray(), row.InitiatorReadMaxDate,
            row.RecipientReadMaxDate, requestedAuthKeys);
    }

    private static TLDto.TLSecretChatState BuildAcceptedChat(
        TLDto.TLSecretChatState source, long recipientAuthKeyId,
        ReadOnlySpan<byte> gB, long keyFingerprint, int date)
    {
        TLDto.SecretChatState row = source.AsSecretChatState();
        return BuildChat(row.HistoryDeleted, recipientAuthKeyId, gB.ToArray(),
            keyFingerprint, row.ChatId, row.AccessHash, row.InitiatorUserId,
            row.RecipientUserId, row.InitiatorAuthKeyId,
            SecretChatPersistenceState.Active, row.CreatedAt, date,
            row.GA.ToArray(), row.InitiatorReadMaxDate, row.RecipientReadMaxDate,
            row.RequestedRecipientAuthKeyIds.ToArray());
    }

    private static TLDto.TLSecretChatState BuildDiscardedChat(
        TLDto.TLSecretChatState source, bool deleteHistory, int date)
    {
        TLDto.SecretChatState row = source.AsSecretChatState();
        long? recipientAuthKeyId = row.Flags[1] ? row.RecipientAuthKeyId : null;
        byte[]? gB = row.Flags[2] ? row.GB.ToArray() : null;
        long? keyFingerprint = row.Flags[3] ? row.KeyFingerprint : null;
        return BuildChat(row.HistoryDeleted || deleteHistory, recipientAuthKeyId, gB,
            keyFingerprint, row.ChatId, row.AccessHash, row.InitiatorUserId,
            row.RecipientUserId, row.InitiatorAuthKeyId,
            SecretChatPersistenceState.Discarded, row.CreatedAt, date,
            row.GA.ToArray(), row.InitiatorReadMaxDate, row.RecipientReadMaxDate,
            row.RequestedRecipientAuthKeyIds.ToArray());
    }

    private static TLDto.TLSecretChatState BuildReadChat(
        TLDto.TLSecretChatState source, bool callerIsInitiator, int maxDate)
    {
        TLDto.SecretChatState row = source.AsSecretChatState();
        return BuildChat(row.HistoryDeleted,
            row.Flags[1] ? row.RecipientAuthKeyId : null,
            row.Flags[2] ? row.GB.ToArray() : null,
            row.Flags[3] ? row.KeyFingerprint : null, row.ChatId, row.AccessHash,
            row.InitiatorUserId, row.RecipientUserId, row.InitiatorAuthKeyId,
            (SecretChatPersistenceState)row.State, row.CreatedAt, row.UpdatedAt,
            row.GA.ToArray(),
            callerIsInitiator ? maxDate : row.InitiatorReadMaxDate,
            callerIsInitiator ? row.RecipientReadMaxDate : maxDate,
            row.RequestedRecipientAuthKeyIds.ToArray());
    }

    private static TLDto.TLSecretChatState BuildPendingWithRequestedKeys(
        TLDto.TLSecretChatState source, int date, IReadOnlyList<long> requestedAuthKeys)
    {
        TLDto.SecretChatState row = source.AsSecretChatState();
        return BuildChat(row.HistoryDeleted, null, null, null, row.ChatId,
            row.AccessHash, row.InitiatorUserId, row.RecipientUserId,
            row.InitiatorAuthKeyId, SecretChatPersistenceState.Pending, row.CreatedAt,
            date, row.GA.ToArray(), row.InitiatorReadMaxDate,
            row.RecipientReadMaxDate, requestedAuthKeys);
    }

    private static TLDto.TLSecretChatState BuildChat(bool historyDeleted,
        long? recipientAuthKeyId, byte[]? gB, long? keyFingerprint, int chatId,
        long accessHash, long initiatorUserId, long recipientUserId,
        long initiatorAuthKeyId, SecretChatPersistenceState state, int createdAt,
        int updatedAt, byte[] gA, int initiatorReadMaxDate, int recipientReadMaxDate,
        IReadOnlyList<long> requestedAuthKeys)
    {
        var keys = new VectorOfLong();
        foreach (long authKeyId in requestedAuthKeys)
        {
            keys.Append(authKeyId);
        }
        TLDto.SecretChatState.TLObjectBuilder builder = TLDto.SecretChatState.Builder()
            .ChatId(chatId)
            .AccessHash(accessHash)
            .InitiatorUserId(initiatorUserId)
            .RecipientUserId(recipientUserId)
            .InitiatorAuthKeyId(initiatorAuthKeyId)
            .State((int)state)
            .CreatedAt(createdAt)
            .UpdatedAt(updatedAt)
            .GA(gA)
            .InitiatorReadMaxDate(initiatorReadMaxDate)
            .RecipientReadMaxDate(recipientReadMaxDate)
            .RequestedRecipientAuthKeyIds(keys);
        if (historyDeleted)
        {
            builder = builder.HistoryDeleted(true);
        }
        if (recipientAuthKeyId is long recipientKey)
        {
            builder = builder.RecipientAuthKeyId(recipientKey);
        }
        if (gB is not null)
        {
            builder = builder.GB(gB);
        }
        if (keyFingerprint is long fingerprint)
        {
            builder = builder.KeyFingerprint(fingerprint);
        }
        return builder.Build();
    }

    private static TLDto.TLSecretChatRequestedDevice BuildRequestedDevice(
        long authKeyId, int chatId, long userId, int date)
    {
        return TLDto.SecretChatRequestedDevice.Builder()
            .AuthKeyId(authKeyId)
            .ChatId(chatId)
            .UserId(userId)
            .Date(date)
            .Build();
    }

    private static TLDto.TLSecretChatControlTarget BuildControlTarget(
        long authKeyId, int chatId, long userId, int date) =>
        TLDto.SecretChatControlTarget.Builder()
            .AuthKeyId(authKeyId)
            .ChatId(chatId)
            .UserId(userId)
            .Date(date)
            .Build();

    private static TLDto.TLSecretChatPendingPair BuildPendingPair(
        TLDto.TLSecretChatState chat)
    {
        TLDto.SecretChatState row = chat.AsSecretChatState();
        return TLDto.SecretChatPendingPair.Builder()
            .InitiatorUserId(row.InitiatorUserId)
            .RecipientUserId(row.RecipientUserId)
            .ChatId(row.ChatId)
            .Build();
    }

    private static TLDto.TLSecretChatQtsPending BuildQtsPending(int chatId,
        long randomId, int date, int expiresAt, ReadOnlySpan<byte> encryptedMessage)
    {
        return TLDto.SecretChatQtsPending.Builder()
            .ChatId(chatId)
            .RandomId(randomId)
            .Date(date)
            .ExpiresAt(expiresAt)
            .EncryptedMessage(encryptedMessage)
            .Build();
    }

    private static TLDto.TLSecretChatQtsState BuildQtsState(long authKeyId,
        int lastPersistedQts, int acknowledgedQts, int queuedEvents, long queuedBytes,
        TLDto.TLSecretChatQtsPending? pending)
        => BuildQtsState(authKeyId, lastPersistedQts, acknowledgedQts, queuedEvents,
            queuedBytes, pending, null);

    private static TLDto.TLSecretChatQtsState BuildQtsState(long authKeyId,
        int lastPersistedQts, int acknowledgedQts, int queuedEvents, long queuedBytes,
        TLDto.TLSecretChatQtsPending? pending,
        TLDto.TLSecretChatSendQtsPending? pendingSend)
    {
        TLDto.SecretChatQtsState.TLObjectBuilder builder =
            TLDto.SecretChatQtsState.Builder()
                .AuthKeyId(authKeyId)
                .LastPersistedQts(lastPersistedQts)
                .AcknowledgedQts(acknowledgedQts)
                .QueuedEvents(queuedEvents)
                .QueuedBytes(queuedBytes);
        if (pending is not null)
        {
            builder = builder.PendingAppend(pending.Value.AsSpan());
        }
        if (pendingSend is not null)
        {
            builder = builder.PendingSend(pendingSend.Value.AsSpan());
        }
        return builder.Build();
    }

    private static TLDto.TLSecretChatSendQtsPending BuildSendQtsPending(
        long senderAuthKeyId, int chatId, long randomId, int date, int expiresAt,
        ReadOnlySpan<byte> encryptedMessage, ReadOnlySpan<byte> result)
    {
        return TLDto.SecretChatSendQtsPending.Builder()
            .SenderAuthKeyId(senderAuthKeyId)
            .ChatId(chatId)
            .RandomId(randomId)
            .Date(date)
            .ExpiresAt(expiresAt)
            .EncryptedMessage(encryptedMessage)
            .Result(result)
            .Build();
    }

    private static TLDto.TLSecretChatQtsEntry BuildQtsEntry(long recipientAuthKeyId,
        int qts, int chatId, long randomId, int date, int expiresAt,
        ReadOnlySpan<byte> encryptedMessage)
    {
        return TLDto.SecretChatQtsEntry.Builder()
            .RecipientAuthKeyId(recipientAuthKeyId)
            .Qts(qts)
            .ChatId(chatId)
            .RandomId(randomId)
            .Date(date)
            .ExpiresAt(expiresAt)
            .EncryptedMessage(encryptedMessage)
            .Build();
    }

    private static TLDto.TLSecretChatSendReceipt BuildSendReceipt(int chatId,
        long senderAuthKeyId, long randomId, int date, int recipientQts,
        ReadOnlySpan<byte> result)
    {
        return TLDto.SecretChatSendReceipt.Builder()
            .ChatId(chatId)
            .SenderAuthKeyId(senderAuthKeyId)
            .RandomId(randomId)
            .Date(date)
            .RecipientQts(recipientQts)
            .Result(result)
            .Build();
    }

    private static TLDto.TLSecretChatRevokedPeer BuildRevokedPeer(int chatId,
        long peerAuthKeyId, long peerUserId, long controlUpdateId)
    {
        return TLDto.SecretChatRevokedPeer.Builder()
            .ChatId(chatId)
            .PeerAuthKeyId(peerAuthKeyId)
            .PeerUserId(peerUserId)
            .ControlUpdateId(controlUpdateId)
            .Build();
    }

    private static TLDto.TLSecretChatControlUpdate BuildControlUpdate(
        TLDto.TLSecretChatControlUpdate source, int offeredStateDate)
    {
        TLDto.SecretChatControlUpdate row = source.AsSecretChatControlUpdate();
        return TLDto.SecretChatControlUpdate.Builder()
            .OfferedStateDate(offeredStateDate)
            .RecipientAuthKeyId(row.RecipientAuthKeyId)
            .UpdateId(row.UpdateId)
            .ChatId(row.ChatId)
            .PeerAuthKeyId(row.PeerAuthKeyId)
            .PeerUserId(row.PeerUserId)
            .Date(row.Date)
            .ExpiresAt(row.ExpiresAt)
            .Update(row.Update)
            .Build();
    }

    private static TLDto.TLSecretChatAuthKeyRevocation BuildRevocation(long authKeyId,
        int date, bool notificationsCompleted,
        IReadOnlyList<TLDto.TLSecretChatRevokedPeer> affectedPeers)
    {
        var peers = new Vector();
        foreach (TLDto.TLSecretChatRevokedPeer peer in affectedPeers)
        {
            peers.AppendTLObject(peer.AsSpan());
        }
        TLDto.SecretChatAuthKeyRevocation.TLObjectBuilder builder =
            TLDto.SecretChatAuthKeyRevocation.Builder()
                .AuthKeyId(authKeyId)
                .Date(date)
                .AffectedPeers(peers);
        if (notificationsCompleted)
        {
            builder = builder.NotificationsCompleted(true);
        }
        return builder.Build();
    }

    private static IReadOnlyList<TLDto.TLSecretChatRevokedPeer> AffectedPeers(
        TLDto.TLSecretChatAuthKeyRevocation revocation)
    {
        var result = new List<TLDto.TLSecretChatRevokedPeer>();
        Vector peers = revocation.AsSecretChatAuthKeyRevocation().AffectedPeers;
        for (int i = 0; i < peers.Count; i++)
        {
            byte[] bytes = peers.ReadTLObject().ToArray();
            result.Add(new TLDto.TLSecretChatRevokedPeer(bytes, 0, bytes.Length));
        }
        return result;
    }

    private static TLDto.TLSecretChatQtsPending? GetPendingAppend(
        TLDto.TLSecretChatQtsState state)
    {
        TLDto.SecretChatQtsState row = state.AsSecretChatQtsState();
        return row.Flags[0] ? row.Get_PendingAppend() : null;
    }

    private static TLDto.TLSecretChatSendQtsPending? GetPendingSend(
        TLDto.TLSecretChatQtsState state)
    {
        TLDto.SecretChatQtsState row = state.AsSecretChatQtsState();
        return row.Flags[1] ? row.Get_PendingSend() : null;
    }

    private static bool IsIdempotentCreate(TLDto.TLSecretChatState left,
        TLDto.TLSecretChatState right)
    {
        TLDto.SecretChatState leftRow = left.AsSecretChatState();
        TLDto.SecretChatState rightRow = right.AsSecretChatState();
        return leftRow.InitiatorUserId == rightRow.InitiatorUserId &&
               leftRow.InitiatorAuthKeyId == rightRow.InitiatorAuthKeyId &&
               leftRow.RecipientUserId == rightRow.RecipientUserId &&
               leftRow.GA.SequenceEqual(rightRow.GA);
    }

    private static bool ControlUpdatesEqual(TLDto.TLSecretChatControlUpdate left,
        TLDto.TLSecretChatControlUpdate right)
    {
        TLDto.SecretChatControlUpdate leftRow = left.AsSecretChatControlUpdate();
        TLDto.SecretChatControlUpdate rightRow = right.AsSecretChatControlUpdate();
        return leftRow.ChatId == rightRow.ChatId &&
               leftRow.PeerAuthKeyId == rightRow.PeerAuthKeyId &&
               leftRow.PeerUserId == rightRow.PeerUserId &&
               leftRow.Date == rightRow.Date &&
               leftRow.ExpiresAt == rightRow.ExpiresAt &&
               leftRow.Update.SequenceEqual(rightRow.Update);
    }

    private static int ChatId(TLDto.TLSecretChatState chat) =>
        chat.AsSecretChatState().ChatId;

    private static long InitiatorAuthKeyId(TLDto.TLSecretChatState chat) =>
        chat.AsSecretChatState().InitiatorAuthKeyId;

    private static long InitiatorUserId(TLDto.TLSecretChatState chat) =>
        chat.AsSecretChatState().InitiatorUserId;

    private static long RecipientUserId(TLDto.TLSecretChatState chat) =>
        chat.AsSecretChatState().RecipientUserId;

    private static int CreatedAt(TLDto.TLSecretChatState chat) =>
        chat.AsSecretChatState().CreatedAt;

    private static SecretChatPersistenceState State(TLDto.TLSecretChatState chat) =>
        (SecretChatPersistenceState)chat.AsSecretChatState().State;

    private static long? RecipientAuthKeyId(TLDto.TLSecretChatState chat)
    {
        TLDto.SecretChatState row = chat.AsSecretChatState();
        return row.Flags[1] ? row.RecipientAuthKeyId : null;
    }

    private static long[] RequestedRecipientAuthKeyIds(
        TLDto.TLSecretChatState chat) =>
        chat.AsSecretChatState().RequestedRecipientAuthKeyIds.ToArray();

    private static TLDto.TLSecretChatState ReadChat(byte[] bytes) =>
        new(bytes, 0, bytes.Length);

    private static TLDto.TLSecretChatRequestedDevice ReadRequestedDevice(byte[] bytes) =>
        new(bytes, 0, bytes.Length);

    private static TLDto.TLSecretChatQtsEntry ReadQtsEntry(byte[] bytes) =>
        new(bytes, 0, bytes.Length);

    private static TLDto.TLSecretChatQtsState ReadQtsState(byte[] bytes) =>
        new(bytes, 0, bytes.Length);

    private static TLDto.TLSecretChatSendReceipt ReadSendReceipt(byte[] bytes) =>
        new(bytes, 0, bytes.Length);

    private static TLDto.TLSecretChatEncryptedFile ReadEncryptedFile(byte[] bytes) =>
        new(bytes, 0, bytes.Length);

    private static TLDto.TLSecretChatEncryptedFileAssociation
        ReadEncryptedFileAssociation(byte[] bytes) => new(bytes, 0, bytes.Length);

    private static TLDto.TLSecretChatControlUpdate ReadControlUpdate(byte[] bytes) =>
        new(bytes, 0, bytes.Length);

    private static TLDto.TLSecretChatControlTarget ReadControlTarget(byte[] bytes) =>
        new(bytes, 0, bytes.Length);

    private static TLDto.TLSecretChatAuthKeyRevocation ReadRevocation(byte[] bytes) =>
        new(bytes, 0, bytes.Length);

    private static SemaphoreSlim[] CreateGates() =>
        Enumerable.Range(0, StripeCount).Select(_ => new SemaphoreSlim(1, 1)).ToArray();

    private async ValueTask<IReadOnlyList<SemaphoreSlim>> AcquireAuthKeyGatesAsync(
        IEnumerable<long> authKeyIds, CancellationToken cancellationToken)
    {
        SemaphoreSlim[] gates = authKeyIds
            .Select(GetAuthKeyGateIndex)
            .Distinct()
            .Order()
            .Select(index => _authKeyGates[index])
            .ToArray();
        int acquired = 0;
        try
        {
            for (; acquired < gates.Length; acquired++)
            {
                await gates[acquired].WaitAsync(cancellationToken);
            }
            return gates;
        }
        catch
        {
            for (int i = acquired - 1; i >= 0; i--)
            {
                gates[i].Release();
            }
            throw;
        }
    }

    private static void ReleaseGates(IReadOnlyList<SemaphoreSlim> gates)
    {
        for (int i = gates.Count - 1; i >= 0; i--)
        {
            gates[i].Release();
        }
    }

    private static int GetAuthKeyGateIndex(long authKeyId) =>
        (int)(unchecked((ulong)authKeyId) % StripeCount);

    private static SemaphoreSlim GetGate(SemaphoreSlim[] gates, long key)
    {
        ulong positive = unchecked((ulong)key);
        return gates[(int)(positive % (uint)gates.Length)];
    }

    private static long CreateControlUpdateId()
    {
        long id;
        do
        {
            id = BitConverter.ToInt64(RandomNumberGenerator.GetBytes(sizeof(long))) &
                 long.MaxValue;
        } while (id == 0);
        return id;
    }

    private async ValueTask FlushAsync(string operation)
    {
        if (!await _flush())
        {
            throw new IOException($"Failed to persist {operation}.");
        }
    }

}