Skip to content

Commit fb455bf

Browse files
committed
Encode message versionstamps as GUIDs
1 parent d2cbdd4 commit fb455bf

6 files changed

Lines changed: 246 additions & 65 deletions

File tree

service/src/main/java/org/whispersystems/textsecuregcm/storage/foundationdb/FoundationDbMessagePublisher.java

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.whispersystems.textsecuregcm.entities.MessageProtos;
1111
import org.whispersystems.textsecuregcm.storage.MessageStreamEntry;
1212
import org.whispersystems.textsecuregcm.util.Pair;
13+
import org.whispersystems.textsecuregcm.util.UUIDUtil;
1314
import reactor.core.publisher.Flux;
1415
import reactor.core.publisher.FluxSink;
1516

@@ -25,6 +26,7 @@
2526
class FoundationDbMessagePublisher {
2627

2728
private final Database database;
29+
private final MessageGuidCodec messageGuidCodec;
2830
/// The maximum number of messages we will fetch per range query operation to avoid excessive memory consumption
2931
private final int maxMessagesPerScan;
3032
/// The end key at which we stop reading messages. For finite publisher, this is just past the end-of-queue key at the
@@ -88,15 +90,18 @@ enum Event {
8890
private static final Logger LOGGER = LoggerFactory.getLogger(FoundationDbMessagePublisher.class);
8991

9092
FoundationDbMessagePublisher(
91-
final KeySelector beginKeyInclusive,
92-
final KeySelector endKeyExclusive,
93-
final Database database,
94-
final int maxMessagesPerScan,
95-
@Nullable final byte[] messagesAvailableWatchKey,
96-
@Nullable final BiConsumer<State, State> stateChangeListener) {
93+
final KeySelector beginKeyInclusive,
94+
final KeySelector endKeyExclusive,
95+
final Database database,
96+
final MessageGuidCodec messageGuidCodec,
97+
final int maxMessagesPerScan,
98+
@Nullable final byte[] messagesAvailableWatchKey,
99+
@Nullable final BiConsumer<State, State> stateChangeListener) {
100+
97101
this.beginKeyCursor = beginKeyInclusive;
98102
this.endKeyExclusive = endKeyExclusive;
99103
this.database = database;
104+
this.messageGuidCodec = messageGuidCodec;
100105
this.maxMessagesPerScan = maxMessagesPerScan;
101106
this.messagesAvailableWatchKey = messagesAvailableWatchKey;
102107
this.terminateOnQueueEmpty = messagesAvailableWatchKey == null;
@@ -116,8 +121,16 @@ public static FoundationDbMessagePublisher createFinitePublisher(
116121
final KeySelector beginKeyInclusive,
117122
final KeySelector endKeyExclusive,
118123
final Database database,
124+
final MessageGuidCodec messageGuidCodec,
119125
final int maxMessagesPerScan) {
120-
return new FoundationDbMessagePublisher(beginKeyInclusive, endKeyExclusive, database, maxMessagesPerScan, null, null);
126+
127+
return new FoundationDbMessagePublisher(beginKeyInclusive,
128+
endKeyExclusive,
129+
database,
130+
messageGuidCodec,
131+
maxMessagesPerScan,
132+
null,
133+
null);
121134
}
122135

123136
/// Creates a [FoundationDbMessagePublisher] that publishes a non-terminating stream of messages from a device queue.
@@ -127,9 +140,17 @@ public static FoundationDbMessagePublisher createInfinitePublisher(
127140
final KeySelector beginKeyInclusive,
128141
final KeySelector endKeyExclusive,
129142
final Database database,
143+
final MessageGuidCodec messageGuidCodec,
130144
final int maxMessagesPerScan,
131145
final byte[] messagesAvailableWatchKey) {
132-
return new FoundationDbMessagePublisher(beginKeyInclusive, endKeyExclusive, database, maxMessagesPerScan, messagesAvailableWatchKey, null);
146+
147+
return new FoundationDbMessagePublisher(beginKeyInclusive,
148+
endKeyExclusive,
149+
database,
150+
messageGuidCodec,
151+
maxMessagesPerScan,
152+
messagesAvailableWatchKey,
153+
null);
133154
}
134155

135156
private synchronized void setState(final State newState, final Event event) {
@@ -222,7 +243,7 @@ protected synchronized void transitionStateOnEvent(final Event event) {
222243
///
223244
/// @return a future of a list of [MessageStreamEntry] with a max size of [#maxMessagesPerScan]
224245
private CompletableFuture<List<MessageStreamEntry.Envelope>> getMessagesBatch() {
225-
return database.runAsync(transaction -> getItemsInRange(transaction, beginKeyCursor, endKeyExclusive, maxMessagesPerScan)
246+
return database.runAsync(transaction -> getItemsInRange(transaction, messageGuidCodec, beginKeyCursor, endKeyExclusive, maxMessagesPerScan)
226247
.thenApply(lastKeyReadAndItems -> {
227248
// Set our beginning key to just past the last key read so that we're ready for our next fetch
228249
lastKeyReadAndItems.first().ifPresent(lastKeyRead -> beginKeyCursor = KeySelector.firstGreaterThan(lastKeyRead));
@@ -248,6 +269,7 @@ private CompletableFuture<List<MessageStreamEntry.Envelope>> getMessagesBatch()
248269
/// @return the last key read (if there were non-zero number of messages read) and the list of messages read
249270
private static CompletableFuture<Pair<Optional<byte[]>, List<MessageStreamEntry.Envelope>>> getItemsInRange(
250271
final Transaction transaction,
272+
final MessageGuidCodec messageGuidCodec,
251273
final KeySelector beginInclusive,
252274
final KeySelector endExclusive,
253275
final int maxMessagesPerScan) {
@@ -259,7 +281,10 @@ private static CompletableFuture<Pair<Optional<byte[]>, List<MessageStreamEntry.
259281
final List<MessageStreamEntry.Envelope> messages = keyValues.stream()
260282
.map(keyValue -> {
261283
try {
262-
return new MessageStreamEntry.Envelope(MessageProtos.Envelope.parseFrom(keyValue.getValue()));
284+
return new MessageStreamEntry.Envelope(MessageProtos.Envelope.parseFrom(keyValue.getValue())
285+
.toBuilder()
286+
.setServerGuidBinary(UUIDUtil.toByteString(messageGuidCodec.encodeMessageGuid(FoundationDbMessageStore.getVersionstamp(keyValue.getKey()))))
287+
.build());
263288
} catch (final InvalidProtocolBufferException e) {
264289
throw new UncheckedIOException(e);
265290
}

service/src/main/java/org/whispersystems/textsecuregcm/storage/foundationdb/FoundationDbMessageStore.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
public class FoundationDbMessageStore {
4141

4242
private final Database[] databases;
43+
private final VersionstampUUIDCipher versionstampUUIDCipher;
4344
private final Executor executor;
4445
private final Clock clock;
4546

@@ -60,8 +61,13 @@ public class FoundationDbMessageStore {
6061
public record InsertResult(Optional<Versionstamp> versionstamp, boolean present) {
6162
}
6263

63-
public FoundationDbMessageStore(final Database[] databases, final Executor executor, final Clock clock) {
64+
public FoundationDbMessageStore(final Database[] databases,
65+
final VersionstampUUIDCipher versionstampUUIDCipher,
66+
final Executor executor,
67+
final Clock clock) {
68+
6469
this.databases = databases;
70+
this.versionstampUUIDCipher = versionstampUUIDCipher;
6571
this.executor = executor;
6672
this.clock = clock;
6773
}
@@ -263,9 +269,14 @@ MessageStream getMessages(final AciServiceIdentifier aci, final Device destinati
263269
return new FoundationDbMessageStream(getDeviceQueueSubspace(aci, destinationDevice.getId()),
264270
getMessagesAvailableWatchKey(aci),
265271
getShardForAci(aci),
272+
new MessageGuidCodec(aci.uuid(), destinationDevice.getId(), versionstampUUIDCipher),
266273
maxMessagesPerScan);
267274
}
268275

276+
static Versionstamp getVersionstamp(final byte[] messageKey) {
277+
return Tuple.fromBytes(messageKey).getVersionstamp(4);
278+
}
279+
269280
@VisibleForTesting
270281
Database getShardForAci(final AciServiceIdentifier aci) {
271282
return databases[hashAciToShardNumber(aci)];
@@ -278,20 +289,20 @@ int hashAciToShardNumber(final AciServiceIdentifier aci) {
278289
}
279290

280291
@VisibleForTesting
281-
Subspace getDeviceQueueSubspace(final AciServiceIdentifier aci, final byte deviceId) {
292+
static Subspace getDeviceQueueSubspace(final AciServiceIdentifier aci, final byte deviceId) {
282293
return getDeviceSubspace(aci, deviceId).get("Q");
283294
}
284295

285-
private Subspace getDeviceSubspace(final AciServiceIdentifier aci, final byte deviceId) {
296+
private static Subspace getDeviceSubspace(final AciServiceIdentifier aci, final byte deviceId) {
286297
return getAccountSubspace(aci).get(deviceId);
287298
}
288299

289-
private Subspace getAccountSubspace(final AciServiceIdentifier aci) {
300+
private static Subspace getAccountSubspace(final AciServiceIdentifier aci) {
290301
return MESSAGES_SUBSPACE.get(aci.uuid());
291302
}
292303

293304
@VisibleForTesting
294-
byte[] getMessagesAvailableWatchKey(final AciServiceIdentifier aci) {
305+
static byte[] getMessagesAvailableWatchKey(final AciServiceIdentifier aci) {
295306
return getAccountSubspace(aci).pack("l");
296307
}
297308

service/src/main/java/org/whispersystems/textsecuregcm/storage/foundationdb/FoundationDbMessageStream.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,23 @@ public class FoundationDbMessageStream implements MessageStream {
2121
private final Subspace deviceQueueSubspace;
2222
private final byte[] messagesAvailableWatchKey;
2323
private final Database database;
24+
private final MessageGuidCodec messageGuidCodec;
2425
/// The maximum number of messages we will fetch per range query operation to avoid excessive memory consumption
2526
private final int maxMessagesPerScan;
2627
private final Flow.Publisher<MessageStreamEntry> messageStreamPublisher;
2728

2829
static final int DEFAULT_MAX_MESSAGES_PER_SCAN = 1024;
2930

30-
FoundationDbMessageStream(final Subspace deviceQueueSubspace, final byte[] messagesAvailableWatchKey,
31-
final Database database, final int maxMessagesPerScan) {
31+
FoundationDbMessageStream(final Subspace deviceQueueSubspace,
32+
final byte[] messagesAvailableWatchKey,
33+
final Database database,
34+
final MessageGuidCodec messageGuidCodec,
35+
final int maxMessagesPerScan) {
36+
3237
this.deviceQueueSubspace = deviceQueueSubspace;
3338
this.messagesAvailableWatchKey = messagesAvailableWatchKey;
3439
this.database = database;
40+
this.messageGuidCodec = messageGuidCodec;
3541
this.maxMessagesPerScan = maxMessagesPerScan;
3642
this.messageStreamPublisher = JdkFlowAdapter.publisherToFlowPublisher(createMessagePublisher());
3743
}
@@ -59,13 +65,13 @@ private Flux<MessageStreamEntry> createMessagePublisher() {
5965
final Flux<MessageStreamEntry.Envelope> finitePublisher = maybeEndOfQueueKeyExclusive
6066
.map(endOfQueueKeyExclusive -> FoundationDbMessagePublisher.createFinitePublisher(
6167
KeySelector.firstGreaterOrEqual(deviceQueueSubspace.range().begin),
62-
endOfQueueKeyExclusive, database, maxMessagesPerScan).getMessages())
68+
endOfQueueKeyExclusive, database, messageGuidCodec, maxMessagesPerScan).getMessages())
6369
.orElseGet(Flux::empty);
6470
final KeySelector infinitePublisherBeginKey = maybeEndOfQueueKeyExclusive.orElseGet(
6571
() -> KeySelector.firstGreaterOrEqual(deviceQueueSubspace.range().begin));
6672
final Flux<MessageStreamEntry.Envelope> infinitePublisher = FoundationDbMessagePublisher.createInfinitePublisher(
6773
infinitePublisherBeginKey, KeySelector.firstGreaterThan(deviceQueueSubspace.range().end),
68-
database, maxMessagesPerScan, messagesAvailableWatchKey).getMessages();
74+
database, messageGuidCodec, maxMessagesPerScan, messagesAvailableWatchKey).getMessages();
6975
return Flux.concat(
7076
finitePublisher,
7177
Mono.just(new MessageStreamEntry.QueueEmpty()),
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2026 Signal Messenger, LLC
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
package org.whispersystems.textsecuregcm.storage.foundationdb;
7+
8+
import com.apple.foundationdb.tuple.Versionstamp;
9+
import java.util.UUID;
10+
11+
class MessageGuidCodec {
12+
13+
private final UUID accountIdentifier;
14+
private final byte deviceId;
15+
private final VersionstampUUIDCipher versionstampUUIDCipher;
16+
17+
MessageGuidCodec(final UUID accountIdentifier,
18+
final byte deviceId,
19+
final VersionstampUUIDCipher versionstampUUIDCipher) {
20+
21+
this.accountIdentifier = accountIdentifier;
22+
this.deviceId = deviceId;
23+
this.versionstampUUIDCipher = versionstampUUIDCipher;
24+
}
25+
26+
public UUID encodeMessageGuid(final Versionstamp versionstamp) {
27+
return versionstampUUIDCipher.encryptVersionstamp(versionstamp, accountIdentifier, deviceId);
28+
}
29+
30+
public Versionstamp decodeMessageGuid(final UUID messageGuid) {
31+
return versionstampUUIDCipher.decryptVersionstamp(messageGuid, accountIdentifier, deviceId);
32+
}
33+
}

0 commit comments

Comments
 (0)