diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 40e66d9913..9d42042bb6 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1186,11 +1186,8 @@ serveRoster :: User -> GroupInfo -> GroupMember -> CM () serveRoster user gInfo member = when (member `supportsVersion` groupRosterVersion) $ do cxt <- chatStoreCxt - -- read the stored blob's own version, not roster_version: roster_version is the acceptance gate (highest - -- version accepted), which a delta can advance past the stored blob when the owner's blob send failed - - -- recording it would over-claim what this member was actually served - withStore' (\db -> (,) <$> getGroupRoster db gInfo <*> getStoredRosterVersion db gInfo) >>= \case - (Just (ownerGMId, brokerTs, sm@SignedMsg {signedBody}, blob_), storedVer_) -> + withStore' (\db -> getStoredGroupRoster db gInfo) >>= \case + Just (ownerGMId, brokerTs, sm@SignedMsg {signedBody}, blob_, storedVer_) -> case J.eitherDecodeStrict' signedBody :: Either String (ChatMessage 'Json) of Left e -> logError $ "serveRoster: cannot decode saved roster message: " <> tshow e Right chatMsg@ChatMessage {msgId} -> @@ -1200,10 +1197,12 @@ 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 (stored) version so a member can't re-trigger a full serve at a version it already got + -- record the blob's own stored version as served, not roster_version (the gate): a delta can + -- advance the gate past the stored blob on a failed blob send, and recording the gate would + -- over-claim what this member was actually served, suppressing legitimate catch-up forM_ storedVer_ $ \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/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 6a2041e5ea..661a0a4967 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -93,7 +93,7 @@ module Simplex.Chat.Store.Groups getStoredRosterVersion, setMemberRosterServedVersion, getMemberRosterServedVersion, - getGroupRoster, + getStoredGroupRoster, RcvRosterTransfer (..), createRosterTransfer, getRosterTransferVersion, @@ -1501,21 +1501,22 @@ 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)) -getGroupRoster db GroupInfo {groupId} = +-- The live roster header a relay re-serves to joiners, with the completed blob and its stored version +-- (all written together at completion, so the blob and version are present whenever the header is). +-- Returns the stored version, not roster_version (the gate), so callers serve/record exactly what they hold. +getStoredGroupRoster :: DB.Connection -> GroupInfo -> IO (Maybe (GroupMemberId, UTCTime, SignedMsg, Maybe ByteString, Maybe VersionRoster)) +getStoredGroupRoster db GroupInfo {groupId} = (>>= toRoster) <$> maybeFirstRow id ( DB.query db - "SELECT roster_sending_owner_gm_id, roster_broker_ts, roster_msg_chat_binding, roster_msg_signatures, roster_msg_body, roster_blob FROM groups WHERE group_id = ?" + "SELECT roster_sending_owner_gm_id, roster_broker_ts, roster_msg_chat_binding, roster_msg_signatures, roster_msg_body, roster_blob, stored_roster_version FROM groups WHERE group_id = ?" (Only groupId) ) where - toRoster (Just ownerGMId, Just brokerTs, Just cb, Just (Binary sigsBs), Just (Binary body), blob_) = - (\sigs -> (ownerGMId, brokerTs, SignedMsg cb sigs body, (\(Binary b) -> b) <$> blob_)) <$> eitherToMaybe (smpDecode sigsBs) + toRoster (Just ownerGMId, Just brokerTs, Just cb, Just (Binary sigsBs), Just (Binary body), blob_, storedVer_) = + (\sigs -> (ownerGMId, brokerTs, SignedMsg cb sigs body, (\(Binary b) -> b) <$> blob_, storedVer_)) <$> eitherToMaybe (smpDecode sigsBs) toRoster _ = Nothing -- A per-source in-flight roster transfer, keyed (group_id, from_member_id): replaces the single