@Operation(
      summary = "Search for the given search keys in the key transparency log",
      description = """
          Enforced unauthenticated endpoint. Returns a response if all search keys exist in the key transparency log.
          """
  )
  @ApiResponse(responseCode = "200", description = "All search key lookups were successful", useReturnTypeSchema = true)
  @ApiResponse(responseCode = "403", description = "At least one search key lookup to value mapping was invalid")
  @ApiResponse(responseCode = "404", description = "At least one search key lookup did not find the key")
  @ApiResponse(responseCode = "413", description = "Ratelimited")
  @ApiResponse(responseCode = "422", description = "Invalid request format")
  @POST
  @Path("/search")
  @RateLimitedByIp(RateLimiters.For.KEY_TRANSPARENCY_SEARCH_PER_IP)
  @Produces(MediaType.APPLICATION_JSON)
  public KeyTransparencySearchResponse search(
      @ReadOnly @Auth final Optional<AuthenticatedDevice> authenticatedAccount,
      @NotNull @Valid final KeyTransparencySearchRequest request) {

    // Disallow clients from making authenticated requests to this endpoint
    requireNotAuthenticated(authenticatedAccount);

    try {
      final CompletableFuture<byte[]> aciSearchKeyResponseFuture = keyTransparencyServiceClient.search(
          getFullSearchKeyByteString(ACI_PREFIX, request.aci().toCompactByteArray()),
          request.lastTreeHeadSize(),
          request.distinguishedTreeHeadSize(),
          KEY_TRANSPARENCY_RPC_TIMEOUT);

      final CompletableFuture<byte[]> e164SearchKeyResponseFuture = request.e164()
          .map(e164 -> keyTransparencyServiceClient.search(
              getFullSearchKeyByteString(E164_PREFIX, e164.getBytes(StandardCharsets.UTF_8)),
              request.lastTreeHeadSize(),
              request.distinguishedTreeHeadSize(),
              KEY_TRANSPARENCY_RPC_TIMEOUT))
          .orElse(CompletableFuture.completedFuture(null));

      final CompletableFuture<byte[]> usernameHashSearchKeyResponseFuture = request.usernameHash()
          .map(usernameHash -> keyTransparencyServiceClient.search(
              getFullSearchKeyByteString(USERNAME_PREFIX, request.usernameHash().get()),
              request.lastTreeHeadSize(),
              request.distinguishedTreeHeadSize(),
              KEY_TRANSPARENCY_RPC_TIMEOUT))
          .orElse(CompletableFuture.completedFuture(null));

      return CompletableFuture.allOf(aciSearchKeyResponseFuture, e164SearchKeyResponseFuture,
              usernameHashSearchKeyResponseFuture)
          .thenApply(ignored ->
              new KeyTransparencySearchResponse(aciSearchKeyResponseFuture.join(),
                  Optional.ofNullable(e164SearchKeyResponseFuture.join()),
                  Optional.ofNullable(usernameHashSearchKeyResponseFuture.join())))
          .join();
    } catch (final CancellationException exception) {
      LOGGER.error("Unexpected cancellation from key transparency service", exception);
      throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE, exception);
    } catch (final CompletionException exception) {
      handleKeyTransparencyServiceError(exception);
    }
    // This is unreachable
    return null;
  }
@Test
  void searchRatelimited() {
    MockUtils.updateRateLimiterResponseToFail(
        rateLimiters, RateLimiters.For.KEY_TRANSPARENCY_SEARCH_PER_IP, "127.0.0.1", Duration.ofMinutes(10), true);
    final Invocation.Builder request = resources.getJerseyTest()
        .target("/v1/key-transparency/search")
        .request();
    try (Response response = request.post(Entity.json(createSearchRequestJson(ACI, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty())))) {
      assertEquals(429, response.getStatus());
      verify(keyTransparencyServiceClient, never()).search(any(), any(), any(), any());
    }
  }