github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/prev.go (about) 1 package chat 2 3 import "github.com/keybase/client/go/protocol/chat1" 4 5 // Ingest a ThreadView, check several invariants, and produce a list of prev 6 // pointers to not-yet-pointed-to messages. Check several invariants at the 7 // same time: 8 // 1. No two messages have the same ID. 9 // 2. All prev pointers point to messages with lesser IDs. 10 // 3. All prev pointers to a message agree on that message's header hash. 11 // 4. For all messages we have locally, the hashes pointing to them are actually correct. 12 // TODO: All of this should happen in the cache instead of here all at once. 13 func CheckPrevPointersAndGetUnpreved(thread *chat1.ThreadView) (newPrevsForRegular, newPrevsForExploding []chat1.MessagePreviousPointer, err ChatThreadConsistencyError) { 14 // Filter out the messages that gave unboxing errors, and index the rest by 15 // ID. Enforce that there are no duplicate IDs or absurd prev pointers. 16 // TODO: What should we really be doing with unboxing errors? Do we worry 17 // about an evil server causing them intentionally? 18 knownMessages := make(map[chat1.MessageID]chat1.MessageUnboxedValid) 19 20 // As part of checking consistency, build up the set of IDs that have never 21 // been preved. There are two views of this set. The regular messages' view 22 // "does not see" exploding messages. (This is an important part of making 23 // them fully repudiable for small teams.) So exploding message IDs will 24 // never show up in the regular unpreved set, and a message is still 25 // considered unpreved in this view if all the prev pointers pointing to it 26 // are exploding. On the other hand, the exploding messages' view sees 27 // everything and treats all messages the same. 28 unprevedIDsForRegular := make(map[chat1.MessageID]struct{}) 29 unprevedIDsForExploding := make(map[chat1.MessageID]struct{}) 30 31 for _, messageOrError := range thread.Messages { 32 if messageOrError.IsValid() { 33 msg := messageOrError.Valid() 34 id := msg.ServerHeader.MessageID 35 36 // Check for IDs that show up more than once. IDs are assigned 37 // sequentially by the server, so this should really never happen. 38 _, alreadyExists := knownMessages[id] 39 if alreadyExists { 40 return nil, nil, NewChatThreadConsistencyError( 41 DuplicateID, 42 "MessageID %d is duplicated", 43 id) 44 } 45 46 // Check that each prev pointer (if any) is a lower ID than the 47 // message itself. 48 for _, prev := range msg.ClientHeader.Prev { 49 if prev.Id >= id { 50 return nil, nil, NewChatThreadConsistencyError( 51 OutOfOrderID, 52 "MessageID %d thinks that message %d is previous.", 53 id, 54 prev.Id) 55 } 56 } 57 58 knownMessages[id] = msg 59 unprevedIDsForExploding[id] = struct{}{} 60 // The regular prev view only sees regular messages. 61 if !msg.IsEphemeral() { 62 unprevedIDsForRegular[id] = struct{}{} 63 } 64 } 65 } 66 67 // Using the index we built above, check each prev pointer on each message 68 // to make sure its hash is correct. Some prev pointers might refer to 69 // messages we don't have locally, and in that case we just check that all 70 // prev pointers to that message are *consistent* with each other. 71 seenHashes := make(map[chat1.MessageID]chat1.Hash) 72 for id, msg := range knownMessages { 73 for _, prev := range msg.ClientHeader.Prev { 74 // If this message has been referred to before, check that it's 75 // always referred to with the same hash. 76 seenHash := seenHashes[prev.Id] 77 if seenHash != nil { 78 // We have seen it before! It's an error if it's different now. 79 if !seenHash.Eq(prev.Hash) { 80 return nil, nil, NewChatThreadConsistencyError( 81 InconsistentHash, 82 "MessageID %d has an inconsistent hash for ID %d (%s and %s)", 83 id, prev.Id, prev.Hash.String(), seenHash.String()) 84 } 85 } else { 86 // We haven't seen it before. Save it. 87 seenHashes[prev.Id] = prev.Hash 88 // And if we do have the previous message in memory, make sure 89 // that what we're saving matches the actual hash for the 90 // message. (Note that HeaderHash is computed *locally* at unbox 91 // time; we're not taking anyone's word for it.) 92 prevMessage, weHaveIt := knownMessages[prev.Id] 93 if weHaveIt && !prevMessage.HeaderHash.Eq(prev.Hash) { 94 return nil, nil, NewChatThreadConsistencyError( 95 IncorrectHash, 96 "Message ID %d thinks message ID %d should have hash %s, but it has %s.", 97 id, prev.Id, prev.Hash.String(), prevMessage.HeaderHash.String()) 98 } 99 // Also remove this message from the set of "never been pointed 100 // to". 101 delete(unprevedIDsForExploding, prev.Id) 102 // The regular prev view doesn't respect exploding prevs. 103 if !msg.IsEphemeral() { 104 delete(unprevedIDsForRegular, prev.Id) 105 } 106 } 107 } 108 } 109 110 newPrevsForRegular = []chat1.MessagePreviousPointer{} 111 for id := range unprevedIDsForRegular { 112 newPrevsForRegular = append(newPrevsForRegular, chat1.MessagePreviousPointer{ 113 Id: id, 114 Hash: knownMessages[id].HeaderHash, 115 }) 116 } 117 118 newPrevsForExploding = []chat1.MessagePreviousPointer{} 119 for id := range unprevedIDsForExploding { 120 newPrevsForExploding = append(newPrevsForExploding, chat1.MessagePreviousPointer{ 121 Id: id, 122 Hash: knownMessages[id].HeaderHash, 123 }) 124 } 125 126 return newPrevsForRegular, newPrevsForExploding, nil 127 }