From 38ce2427b284e15bf1387d80fd307884a19a52c7 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 29 Jun 2026 18:14:46 +0400 Subject: [PATCH] wip --- simplex-chat.cabal | 2 ++ src/Simplex/Chat/Library/Internal.hs | 9 ++++++--- src/Simplex/Chat/Library/Subscriber.hs | 11 +++++++---- src/Simplex/Chat/Store/Groups.hs | 14 ++++++++++++++ src/Simplex/Chat/Store/Postgres/Migrations.hs | 4 +++- .../M20260629_member_roster_served.hs | 19 +++++++++++++++++++ src/Simplex/Chat/Store/SQLite/Migrations.hs | 4 +++- .../M20260629_member_roster_served.hs | 18 ++++++++++++++++++ 8 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 src/Simplex/Chat/Store/Postgres/Migrations/M20260629_member_roster_served.hs create mode 100644 src/Simplex/Chat/Store/SQLite/Migrations/M20260629_member_roster_served.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index f8c599c80b..2577c86deb 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -144,6 +144,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at Simplex.Chat.Store.Postgres.Migrations.M20260601_relay_sent_web_domain Simplex.Chat.Store.Postgres.Migrations.M20260602_group_roster + Simplex.Chat.Store.Postgres.Migrations.M20260629_member_roster_served else exposed-modules: Simplex.Chat.Archive @@ -306,6 +307,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at Simplex.Chat.Store.SQLite.Migrations.M20260601_relay_sent_web_domain Simplex.Chat.Store.SQLite.Migrations.M20260602_group_roster + Simplex.Chat.Store.SQLite.Migrations.M20260629_member_roster_served other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index f84359ee6a..60862a3e3e 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1186,8 +1186,9 @@ serveRoster :: User -> GroupInfo -> GroupMember -> CM () serveRoster user gInfo member = when (member `supportsVersion` groupRosterVersion) $ do cxt <- chatStoreCxt - withStore' (\db -> getGroupRoster db gInfo) >>= \case - Just (ownerGMId, brokerTs, sm@SignedMsg {signedBody}, blob_) -> + -- read the served version with the blob, so the recorded served version matches what is served + withStore' (\db -> (,) <$> getGroupRoster db gInfo <*> getGroupRosterVersion db gInfo) >>= \case + (Just (ownerGMId, brokerTs, sm@SignedMsg {signedBody}, blob_), rosterVer_) -> case J.eitherDecodeStrict' signedBody :: Either String (ChatMessage 'Json) of Left e -> logError $ "serveRoster: cannot decode saved roster message: " <> tshow e Right chatMsg@ChatMessage {msgId} -> @@ -1197,8 +1198,10 @@ serveRoster user gInfo member = sendFwdMemberMessage member fwd (VMSigned MSSVerified sm chatMsg) forM_ ((,) <$> msgId <*> blob_) $ \(sid, blob) -> sendInlineBlobChunks user gInfo [member] sid blob + -- record the served version so a member can't re-trigger a full serve at a version it already got + forM_ rosterVer_ $ \v -> withStore' $ \db -> setMemberRosterServedVersion db member v Left e -> logError $ "serveRoster: roster owner not found: " <> tshow e - Nothing -> pure () + (Nothing, _) -> pure () -- Used in groups with relays to introduce moderators and above to a new member, -- and to announce the new member to moderators and above. diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 0dea4b58f3..e79ccbec64 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -3497,13 +3497,16 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = _ -> pure () -- A relay re-serves the full roster to a subscriber that detected a version gap, but only when it holds - -- something newer than the requester's pre-gap version (rate-limiting same-version requests). serveRoster - -- is the join/resume path: signed header + inline blob chunks to the one requester; a no-op without a roster. + -- something newer than BOTH the requester's claimed version and the version it last served this member - + -- the latter bounds reflected amplification (a member can't re-trigger a full serve with reqVer = 0). + -- serveRoster records the served version (on all serve paths) and is a no-op without a roster. xGrpRosterRequest :: GroupInfo -> GroupMember -> VersionRoster -> CM () xGrpRosterRequest gInfo m reqVer = when (isUserGrpFwdRelay gInfo) $ do - cur <- withStore' $ \db -> getGroupRosterVersion db gInfo - when (maybe True (> reqVer) cur) $ serveRoster user gInfo m + (cur_, served_) <- withStore' $ \db -> + (,) <$> getGroupRosterVersion db gInfo <*> getMemberRosterServedVersion db m + forM_ cur_ $ \cur -> + when (cur > reqVer && maybe True (cur >) served_) $ serveRoster user gInfo m checkHostRole :: GroupMember -> GroupMemberRole -> CM () checkHostRole GroupMember {memberRole, localDisplayName} memRole = diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 60531cc1dc..d39f027e55 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -90,6 +90,8 @@ module Simplex.Chat.Store.Groups getPublishableGroupRelays, setGroupRosterVersion, getGroupRosterVersion, + setMemberRosterServedVersion, + getMemberRosterServedVersion, getGroupRoster, RcvRosterTransfer (..), createRosterTransfer, @@ -1478,6 +1480,18 @@ getGroupRosterVersion db GroupInfo {groupId} = fmap join . maybeFirstRow fromOnly $ DB.query db "SELECT roster_version FROM groups WHERE group_id = ?" (Only groupId) +-- The newest roster version a relay re-served to this member on its catch-up request: bounds reflected +-- amplification, so a member can't re-trigger a full serve at a version it was already served. +setMemberRosterServedVersion :: DB.Connection -> GroupMember -> VersionRoster -> IO () +setMemberRosterServedVersion db GroupMember {groupMemberId} v = do + currentTs <- getCurrentTime + DB.execute db "UPDATE group_members SET roster_served_version = ?, updated_at = ? WHERE group_member_id = ?" (v, currentTs, groupMemberId) + +getMemberRosterServedVersion :: DB.Connection -> GroupMember -> IO (Maybe VersionRoster) +getMemberRosterServedVersion db GroupMember {groupMemberId} = + fmap join . maybeFirstRow fromOnly $ + DB.query db "SELECT roster_served_version FROM group_members WHERE group_member_id = ?" (Only groupMemberId) + -- The live roster header a relay re-serves to joiners, with the completed blob served alongside it -- (both are written together at completion, so the blob is present whenever the header is). getGroupRoster :: DB.Connection -> GroupInfo -> IO (Maybe (GroupMemberId, UTCTime, SignedMsg, Maybe ByteString)) diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 4b814d0434..842a390d5a 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -38,6 +38,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260530_client_services import Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at import Simplex.Chat.Store.Postgres.Migrations.M20260601_relay_sent_web_domain import Simplex.Chat.Store.Postgres.Migrations.M20260602_group_roster +import Simplex.Chat.Store.Postgres.Migrations.M20260629_member_roster_served import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -75,7 +76,8 @@ schemaMigrations = ("20260530_client_services", m20260530_client_services, Just down_m20260530_client_services), ("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at), ("20260601_relay_sent_web_domain", m20260601_relay_sent_web_domain, Just down_m20260601_relay_sent_web_domain), - ("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster) + ("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster), + ("20260629_member_roster_served", m20260629_member_roster_served, Just down_m20260629_member_roster_served) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260629_member_roster_served.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260629_member_roster_served.hs new file mode 100644 index 0000000000..36e218cbd1 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260629_member_roster_served.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260629_member_roster_served where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260629_member_roster_served :: Text +m20260629_member_roster_served = + [r| +ALTER TABLE group_members ADD COLUMN roster_served_version BIGINT; +|] + +down_m20260629_member_roster_served :: Text +down_m20260629_member_roster_served = + [r| +ALTER TABLE group_members DROP COLUMN roster_served_version; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index c9dd316aee..64ec77d320 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -161,6 +161,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260530_client_services import Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at import Simplex.Chat.Store.SQLite.Migrations.M20260601_relay_sent_web_domain import Simplex.Chat.Store.SQLite.Migrations.M20260602_group_roster +import Simplex.Chat.Store.SQLite.Migrations.M20260629_member_roster_served import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -321,7 +322,8 @@ schemaMigrations = ("20260530_client_services", m20260530_client_services, Just down_m20260530_client_services), ("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at), ("20260601_relay_sent_web_domain", m20260601_relay_sent_web_domain, Just down_m20260601_relay_sent_web_domain), - ("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster) + ("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster), + ("20260629_member_roster_served", m20260629_member_roster_served, Just down_m20260629_member_roster_served) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260629_member_roster_served.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260629_member_roster_served.hs new file mode 100644 index 0000000000..a37d05d771 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260629_member_roster_served.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260629_member_roster_served where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260629_member_roster_served :: Query +m20260629_member_roster_served = + [sql| +ALTER TABLE group_members ADD COLUMN roster_served_version INTEGER; +|] + +down_m20260629_member_roster_served :: Query +down_m20260629_member_roster_served = + [sql| +ALTER TABLE group_members DROP COLUMN roster_served_version; +|]