diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java index e9abaf518..e68f96138 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java @@ -549,6 +549,17 @@ public class AccountsManager { return CompletableFuture.failedFuture(new UsernameHashNotAvailableException()); } + if (account.getUsernameHash().filter( + oldHash -> requestedUsernameHashes.stream().anyMatch(hash -> Arrays.equals(oldHash, hash))) + .isPresent()) { + // if we are trying to reserve our already-confirmed username hash, we don't need to do + // anything, and can give the client a success response (they may try to confirm it again, + // but that's a no-op other than rotaing their username link which they may need to do + // anyway). note this is *not* the case for reserving our already-reserved username hash, + // which should extend the reservation's TTL. + return CompletableFuture.completedFuture(new UsernameReservation(account, account.getUsernameHash().get())); + } + final AtomicReference reservedUsernameHash = new AtomicReference<>(); return redisDeleteAsync(account) diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java index 37d62ec73..1f2674e6a 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java @@ -1358,6 +1358,20 @@ class AccountsManagerTest { verify(accounts, times(1)).reserveUsernameHash(eq(account), any(), eq(Duration.ofMinutes(5))); } + @Test + void testReserveOwnUsernameHash() throws UsernameHashNotAvailableException { + final byte[] oldUsernameHash = TestRandomUtil.nextBytes(32); + final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]); + account.setUsernameHash(oldUsernameHash); + when(accounts.getByAccountIdentifierAsync(account.getUuid())).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + final List usernameHashes = List.of(TestRandomUtil.nextBytes(32), oldUsernameHash, TestRandomUtil.nextBytes(32)); + + UsernameReservation result = accountsManager.reserveUsernameHash(account, usernameHashes).join(); + assertArrayEquals(oldUsernameHash, result.reservedUsernameHash()); + verify(accounts, never()).reserveUsernameHash(any(), any(), any()); + } + @Test void testReserveUsernameOptimisticLockingFailure() throws UsernameHashNotAvailableException { final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);