diff --git a/build/repositories.bzl b/build/repositories.bzl index 025c46eaba3..4a21a64aeef 100644 --- a/build/repositories.bzl +++ b/build/repositories.bzl @@ -41,8 +41,8 @@ def wfa_measurement_system_repositories(): wfa_repo_archive( name = "wfa_measurement_proto", repo = "cross-media-measurement-api", - sha256 = "3ccf5e4e81f2b0cd9abfc0fe9945096e6ff1c18577a9d9f67ea60470c64c3ec3", - version = "0.39.1", + sha256 = "642106dd7c10b4c8820c31c3c18f54e7a5b9480adc85b9f6e58b267fd8f7a62e", + commit = "cf1af8937a764c491e1c99a79193ac677381c03a", ) wfa_repo_archive( diff --git a/src/main/kotlin/org/wfanet/measurement/integration/common/BUILD.bazel b/src/main/kotlin/org/wfanet/measurement/integration/common/BUILD.bazel index dea216d8bca..6a0b389fd4e 100644 --- a/src/main/kotlin/org/wfanet/measurement/integration/common/BUILD.bazel +++ b/src/main/kotlin/org/wfanet/measurement/integration/common/BUILD.bazel @@ -77,6 +77,7 @@ kt_jvm_library( "//src/main/kotlin/org/wfanet/measurement/kingdom/deploy/common/service:data_services", "//src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha:api_key_authentication_server_interceptor", "//src/main/kotlin/org/wfanet/measurement/loadtest/panelmatchresourcesetup", + "//src/main/proto/wfa/measurement/api/v2alpha:protocol_config_kt_jvm_proto", "@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common/grpc", "@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common/grpc/testing", "@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common/testing", diff --git a/src/main/kotlin/org/wfanet/measurement/integration/common/InProcessKingdom.kt b/src/main/kotlin/org/wfanet/measurement/integration/common/InProcessKingdom.kt index 325620f123b..7be1402a412 100644 --- a/src/main/kotlin/org/wfanet/measurement/integration/common/InProcessKingdom.kt +++ b/src/main/kotlin/org/wfanet/measurement/integration/common/InProcessKingdom.kt @@ -62,6 +62,7 @@ import org.wfanet.measurement.kingdom.service.system.v1alpha.ComputationLogEntri import org.wfanet.measurement.kingdom.service.system.v1alpha.ComputationParticipantsService as SystemComputationParticipantsService import org.wfanet.measurement.kingdom.service.system.v1alpha.ComputationsService as SystemComputationsService import org.wfanet.measurement.kingdom.service.system.v1alpha.RequisitionsService as SystemRequisitionsService +import org.wfanet.measurement.api.v2alpha.ProtocolConfig import org.wfanet.measurement.loadtest.panelmatchresourcesetup.PanelMatchResourceSetup /** TestRule that starts and stops all Kingdom gRPC services. */ @@ -150,7 +151,7 @@ class InProcessKingdom( EventGroupMetadataDescriptorsService(internalEventGroupMetadataDescriptorsClient) .withMetadataPrincipalIdentities() .withApiKeyAuthenticationServerInterceptor(internalApiKeysClient), - MeasurementsService(internalMeasurementsClient) + MeasurementsService(internalMeasurementsClient, measurementNoiseMechanisms) .withMetadataPrincipalIdentities() .withApiKeyAuthenticationServerInterceptor(internalApiKeysClient), PublicKeysService(internalPublicKeysClient) @@ -206,5 +207,11 @@ class InProcessKingdom( /** Default deadline for RPCs to internal server in milliseconds. */ private const val DEFAULT_INTERNAL_DEADLINE_MILLIS = 30_000L + private val measurementNoiseMechanisms: List = + listOf( + ProtocolConfig.NoiseMechanism.NONE, + ProtocolConfig.NoiseMechanism.GEOMETRIC, + ProtocolConfig.NoiseMechanism.DISCRETE_GAUSSIAN, + ) } } diff --git a/src/main/kotlin/org/wfanet/measurement/kingdom/deploy/common/server/V2alphaPublicApiServer.kt b/src/main/kotlin/org/wfanet/measurement/kingdom/deploy/common/server/V2alphaPublicApiServer.kt index a90b1d7e1d4..e175f9d237c 100644 --- a/src/main/kotlin/org/wfanet/measurement/kingdom/deploy/common/server/V2alphaPublicApiServer.kt +++ b/src/main/kotlin/org/wfanet/measurement/kingdom/deploy/common/server/V2alphaPublicApiServer.kt @@ -17,6 +17,7 @@ package org.wfanet.measurement.kingdom.deploy.common.server import io.grpc.ServerServiceDefinition import java.io.File import org.wfanet.measurement.api.v2alpha.AkidPrincipalLookup +import org.wfanet.measurement.api.v2alpha.ProtocolConfig import org.wfanet.measurement.api.v2alpha.withPrincipalsFromX509AuthorityKeyIdentifiers import org.wfanet.measurement.common.commandLineMain import org.wfanet.measurement.common.crypto.SigningCerts @@ -109,6 +110,19 @@ private fun run( .withDefaultDeadline(kingdomApiServerFlags.internalApiFlags.defaultDeadlineDuration) val principalLookup = AkidPrincipalLookup(v2alphaFlags.authorityKeyIdentifierToPrincipalMapFile) + val noiseMechanisms = mutableListOf() + if (v2alphaFlags.directNoiseMechanismInput.noNoise) { + noiseMechanisms += ProtocolConfig.NoiseMechanism.NONE + } + if (v2alphaFlags.directNoiseMechanismInput.geometryNoise) { + noiseMechanisms += ProtocolConfig.NoiseMechanism.GEOMETRIC + } + if (v2alphaFlags.directNoiseMechanismInput.discreteGaussianNoise) { + noiseMechanisms += ProtocolConfig.NoiseMechanism.DISCRETE_GAUSSIAN + } + if (noiseMechanisms.size == 0) { + error("No noise mechanism is selected.") + } val internalAccountsCoroutineStub = InternalAccountsCoroutineStub(channel) val internalApiKeysCoroutineStub = InternalApiKeysCoroutineStub(channel) @@ -141,9 +155,7 @@ private fun run( ) .withPrincipalsFromX509AuthorityKeyIdentifiers(principalLookup) .withApiKeyAuthenticationServerInterceptor(internalApiKeysCoroutineStub), - MeasurementsService( - InternalMeasurementsCoroutineStub(channel), - ) + MeasurementsService(InternalMeasurementsCoroutineStub(channel), noiseMechanisms) .withPrincipalsFromX509AuthorityKeyIdentifiers(principalLookup) .withApiKeyAuthenticationServerInterceptor(internalApiKeysCoroutineStub), MeasurementConsumersService(InternalMeasurementConsumersCoroutineStub(channel)) @@ -192,6 +204,32 @@ fun main(args: Array) = commandLineMain(::run, args) /** Flags specific to the V2alpha API version. */ private class V2alphaFlags { + class DirectNoiseMechanismInput { + @CommandLine.Option( + names = ["--none"], + description = ["Allow no noise added to the result of direct computation."], + required = false + ) + var noNoise = false + private set + + @CommandLine.Option( + names = ["--geometry"], + description = ["Allow geometry (Laplace) noise added to the result of direct computation."], + required = false + ) + var geometryNoise = false + private set + + @CommandLine.Option( + names = ["--discrete-gaussian"], + description = ["Allow discrete Gaussian noise added to the result of direct computation."], + required = false + ) + var discreteGaussianNoise = false + private set + } + @CommandLine.Option( names = ["--authority-key-identifier-to-principal-map-file"], description = ["File path to a AuthorityKeyToPrincipalMap textproto"], @@ -207,4 +245,8 @@ private class V2alphaFlags { ) lateinit var redirectUri: String private set + + @CommandLine.ArgGroup(exclusive = true, multiplicity = "1", heading = "Direct noise mechanisms\n") + lateinit var directNoiseMechanismInput: DirectNoiseMechanismInput + private set } diff --git a/src/main/kotlin/org/wfanet/measurement/kingdom/deploy/gcloud/spanner/writers/CreateMeasurement.kt b/src/main/kotlin/org/wfanet/measurement/kingdom/deploy/gcloud/spanner/writers/CreateMeasurement.kt index 314b5636305..fd010b94bd5 100644 --- a/src/main/kotlin/org/wfanet/measurement/kingdom/deploy/gcloud/spanner/writers/CreateMeasurement.kt +++ b/src/main/kotlin/org/wfanet/measurement/kingdom/deploy/gcloud/spanner/writers/CreateMeasurement.kt @@ -88,8 +88,9 @@ class CreateMeasurement(private val request: CreateMeasurementRequest) : ProtocolConfig.ProtocolCase.REACH_ONLY_LIQUID_LEGIONS_V2 -> { createComputedMeasurement(request.measurement, measurementConsumerId) } - ProtocolConfig.ProtocolCase.PROTOCOL_NOT_SET -> + ProtocolConfig.ProtocolCase.DIRECT -> createDirectMeasurement(request.measurement, measurementConsumerId) + ProtocolConfig.ProtocolCase.PROTOCOL_NOT_SET -> error("Protocol is not set.") } } diff --git a/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/ExchangeStepsService.kt b/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/ExchangeStepsService.kt index a45c2eeca4f..5cc73548f25 100644 --- a/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/ExchangeStepsService.kt +++ b/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/ExchangeStepsService.kt @@ -160,7 +160,7 @@ class ExchangeStepsService(private val internalExchangeSteps: InternalExchangeSt it.toV2Alpha() } catch (e: Throwable) { failGrpc(Status.INVALID_ARGUMENT) { - e.message ?: "Failed to convert ProtocolConfig ExchangeStep" + e.message ?: "Failed to convert ExchangeStep" } } } diff --git a/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/MeasurementsService.kt b/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/MeasurementsService.kt index 27e0bd86728..5d8821e6340 100644 --- a/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/MeasurementsService.kt +++ b/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/MeasurementsService.kt @@ -41,6 +41,7 @@ import org.wfanet.measurement.api.v2alpha.MeasurementKey import org.wfanet.measurement.api.v2alpha.MeasurementPrincipal import org.wfanet.measurement.api.v2alpha.MeasurementSpec import org.wfanet.measurement.api.v2alpha.MeasurementsGrpcKt.MeasurementsCoroutineImplBase +import org.wfanet.measurement.api.v2alpha.ProtocolConfig.NoiseMechanism import org.wfanet.measurement.api.v2alpha.copy import org.wfanet.measurement.api.v2alpha.listMeasurementsPageToken import org.wfanet.measurement.api.v2alpha.listMeasurementsResponse @@ -72,6 +73,7 @@ private const val MISSING_RESOURCE_NAME_ERROR = "Resource name is either unspeci class MeasurementsService( private val internalMeasurementsStub: MeasurementsCoroutineStub, + private val noiseMechanisms: List ) : MeasurementsCoroutineImplBase() { override suspend fun getMeasurement(request: GetMeasurementRequest): Measurement { @@ -167,7 +169,8 @@ class MeasurementsService( request.measurement.toInternal( measurementConsumerCertificateKey, dataProvidersMap, - parsedMeasurementSpec + parsedMeasurementSpec, + noiseMechanisms.map { it.toInternal() } ) requestId = request.requestId } diff --git a/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/ProtoConversions.kt b/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/ProtoConversions.kt index c22a0dd5b92..dac3dc2664c 100644 --- a/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/ProtoConversions.kt +++ b/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/ProtoConversions.kt @@ -15,7 +15,6 @@ package org.wfanet.measurement.kingdom.service.api.v2alpha import com.google.protobuf.util.Timestamps -import com.google.type.date import com.google.type.interval import java.time.ZoneOffset import org.wfanet.measurement.api.Version @@ -61,6 +60,7 @@ import org.wfanet.measurement.api.v2alpha.ModelSuite import org.wfanet.measurement.api.v2alpha.ModelSuiteKey import org.wfanet.measurement.api.v2alpha.ProtocolConfig import org.wfanet.measurement.api.v2alpha.ProtocolConfig.NoiseMechanism +import org.wfanet.measurement.api.v2alpha.ProtocolConfigKt import org.wfanet.measurement.api.v2alpha.ProtocolConfigKt.direct import org.wfanet.measurement.api.v2alpha.ProtocolConfigKt.liquidLegionsV2 import org.wfanet.measurement.api.v2alpha.ProtocolConfigKt.protocol @@ -107,6 +107,7 @@ import org.wfanet.measurement.internal.kingdom.ModelShard as InternalModelShard import org.wfanet.measurement.internal.kingdom.ModelSuite as InternalModelSuite import org.wfanet.measurement.internal.kingdom.ProtocolConfig as InternalProtocolConfig import org.wfanet.measurement.internal.kingdom.ProtocolConfig.NoiseMechanism as InternalNoiseMechanism +import org.wfanet.measurement.internal.kingdom.ProtocolConfigKt as InternalProtocolConfigKt import org.wfanet.measurement.internal.kingdom.duchyProtocolConfig import org.wfanet.measurement.internal.kingdom.exchangeWorkflow import org.wfanet.measurement.internal.kingdom.measurement as internalMeasurement @@ -120,6 +121,10 @@ import org.wfanet.measurement.internal.kingdom.protocolConfig as internalProtoco import org.wfanet.measurement.kingdom.deploy.common.Llv2ProtocolConfig import org.wfanet.measurement.kingdom.deploy.common.RoLlv2ProtocolConfig +// (-- TODO(world-federation-of-advertisers/cross-media-measurement-api/issues/160): this value +// won't be needed once the maximum frequency field is moved to measurement spec. --) +const val DEFAULT_MAXIMUM_FREQUENCY_DIRECT_DISTRIBUTION = 20 + /** Converts an internal [InternalMeasurement.State] to a public [State]. */ fun InternalMeasurement.State.toState(): State = when (this) { @@ -182,11 +187,23 @@ fun InternalNoiseMechanism.toNoiseMechanism(): NoiseMechanism { return when (this) { InternalNoiseMechanism.GEOMETRIC -> NoiseMechanism.GEOMETRIC InternalNoiseMechanism.DISCRETE_GAUSSIAN -> NoiseMechanism.DISCRETE_GAUSSIAN + InternalNoiseMechanism.NONE -> NoiseMechanism.NONE InternalNoiseMechanism.NOISE_MECHANISM_UNSPECIFIED, InternalNoiseMechanism.UNRECOGNIZED -> error("invalid internal noise mechanism.") } } +/** Converts a public [NoiseMechanism] to an internal [InternalNoiseMechanism]. */ +fun NoiseMechanism.toInternal(): InternalNoiseMechanism { + return when (this) { + NoiseMechanism.GEOMETRIC -> InternalNoiseMechanism.GEOMETRIC + NoiseMechanism.DISCRETE_GAUSSIAN -> InternalNoiseMechanism.DISCRETE_GAUSSIAN + NoiseMechanism.NONE -> InternalNoiseMechanism.NONE + NoiseMechanism.NOISE_MECHANISM_UNSPECIFIED, + NoiseMechanism.UNRECOGNIZED -> error("invalid internal noise mechanism.") + } +} + /** Converts an internal [InternalProtocolConfig] to a public [ProtocolConfig]. */ fun InternalProtocolConfig.toProtocolConfig( measurementTypeCase: MeasurementSpec.MeasurementTypeCase, @@ -211,10 +228,23 @@ fun InternalProtocolConfig.toProtocolConfig( ProtocolConfig.MeasurementType.REACH, ProtocolConfig.MeasurementType.REACH_AND_FREQUENCY -> { if (dataProviderCount == 1) { - protocols += protocol { direct = direct {} } + protocols += protocol { + if (source.hasDirect()) { + direct = source.direct.toDirect() + } else { + // For backward compatibility + direct = direct {} + } + } } else { @Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") // Protobuf enum fields are never null. when (source.protocolCase) { + InternalProtocolConfig.ProtocolCase.DIRECT -> { + error( + "Direct protocol of reach computation shouldn't be used when number of data " + + "providers is greater than 1." + ) + } InternalProtocolConfig.ProtocolCase.LIQUID_LEGIONS_V2 -> { protocols += protocol { liquidLegionsV2 = liquidLegionsV2 { @@ -288,6 +318,45 @@ fun InternalProtocolConfig.toProtocolConfig( } } +/** + * Converts an internal [InternalProtocolConfig.Direct] to a public [InternalProtocolConfig.Direct]. + */ +private fun InternalProtocolConfig.Direct.toDirect(): ProtocolConfig.Direct { + val source = this + + return direct { + noiseMechanisms += + source.noiseMechanismsList.map { internalNoiseMechanism -> + internalNoiseMechanism.toNoiseMechanism() + } + + if (source.hasDeterministicCountDistinct()) { + deterministicCountDistinct = ProtocolConfigKt.DirectKt.deterministicCountDistinct {} + } + if (source.hasDeterministicDistribution()) { + deterministicDistribution = + ProtocolConfigKt.DirectKt.deterministicDistribution { + maximumFrequency = source.deterministicDistribution.maximumFrequency + } + } + if (source.hasDeterministicCount()) { + deterministicCount = ProtocolConfigKt.DirectKt.deterministicCount {} + } + if (source.hasDeterministicSum()) { + deterministicSum = ProtocolConfigKt.DirectKt.deterministicSum {} + } + if (source.hasLiquidLegionsCountDistinct()) { + liquidLegionsCountDistinct = ProtocolConfigKt.DirectKt.liquidLegionsCountDistinct {} + } + if (source.hasLiquidLegionsDistribution()) { + liquidLegionsDistribution = + ProtocolConfigKt.DirectKt.liquidLegionsDistribution { + maximumFrequency = source.liquidLegionsDistribution.maximumFrequency + } + } + } +} + /** Converts an internal [InternalModelSuite] to a public [ModelSuite]. */ fun InternalModelSuite.toModelSuite(): ModelSuite { val source = this @@ -709,7 +778,8 @@ fun Map.Entry.toDataProviderEntry(): DataProviderEntry fun Measurement.toInternal( measurementConsumerCertificateKey: MeasurementConsumerCertificateKey, dataProvidersMap: Map, - measurementSpecProto: MeasurementSpec + measurementSpecProto: MeasurementSpec, + internalNoiseMechanisms: List ): InternalMeasurement { val publicMeasurement = this @@ -746,6 +816,17 @@ fun Measurement.toInternal( liquidLegionsV2 = Llv2ProtocolConfig.duchyProtocolConfig } } + } else if (dataProvidersCount == 1) { + protocolConfig = internalProtocolConfig { + direct = + InternalProtocolConfigKt.direct { + this.noiseMechanisms += internalNoiseMechanisms + deterministicCountDistinct = + InternalProtocolConfigKt.DirectKt.deterministicCountDistinct {} + liquidLegionsCountDistinct = + InternalProtocolConfigKt.DirectKt.liquidLegionsCountDistinct {} + } + } } } MeasurementSpec.MeasurementTypeCase.REACH_AND_FREQUENCY -> { @@ -757,10 +838,49 @@ fun Measurement.toInternal( duchyProtocolConfig = duchyProtocolConfig { liquidLegionsV2 = Llv2ProtocolConfig.duchyProtocolConfig } + } else if (dataProvidersCount == 1) { + protocolConfig = internalProtocolConfig { + direct = + InternalProtocolConfigKt.direct { + this.noiseMechanisms += internalNoiseMechanisms + deterministicCountDistinct = + InternalProtocolConfigKt.DirectKt.deterministicCountDistinct {} + liquidLegionsCountDistinct = + InternalProtocolConfigKt.DirectKt.liquidLegionsCountDistinct {} + deterministicDistribution = + InternalProtocolConfigKt.DirectKt.deterministicDistribution { + maximumFrequency = DEFAULT_MAXIMUM_FREQUENCY_DIRECT_DISTRIBUTION + } + liquidLegionsDistribution = + InternalProtocolConfigKt.DirectKt.liquidLegionsDistribution { + maximumFrequency = DEFAULT_MAXIMUM_FREQUENCY_DIRECT_DISTRIBUTION + } + } + } + } + } + MeasurementSpec.MeasurementTypeCase.IMPRESSION -> { + if (dataProvidersCount == 1) { + protocolConfig = internalProtocolConfig { + direct = + InternalProtocolConfigKt.direct { + this.noiseMechanisms += internalNoiseMechanisms + deterministicCount = InternalProtocolConfigKt.DirectKt.deterministicCount {} + } + } + } + } + MeasurementSpec.MeasurementTypeCase.DURATION -> { + if (dataProvidersCount == 1) { + protocolConfig = internalProtocolConfig { + direct = + InternalProtocolConfigKt.direct { + this.noiseMechanisms += internalNoiseMechanisms + deterministicSum = InternalProtocolConfigKt.DirectKt.deterministicSum {} + } + } } } - MeasurementSpec.MeasurementTypeCase.IMPRESSION, - MeasurementSpec.MeasurementTypeCase.DURATION, -> {} MeasurementSpec.MeasurementTypeCase.MEASUREMENTTYPE_NOT_SET -> error("MeasurementType not set.") } diff --git a/src/main/kotlin/org/wfanet/measurement/kingdom/service/system/v1alpha/ProtoConversions.kt b/src/main/kotlin/org/wfanet/measurement/kingdom/service/system/v1alpha/ProtoConversions.kt index f400f2a46d7..945c6db9350 100644 --- a/src/main/kotlin/org/wfanet/measurement/kingdom/service/system/v1alpha/ProtoConversions.kt +++ b/src/main/kotlin/org/wfanet/measurement/kingdom/service/system/v1alpha/ProtoConversions.kt @@ -28,6 +28,7 @@ import org.wfanet.measurement.internal.kingdom.MeasurementLogEntry import org.wfanet.measurement.internal.kingdom.ProtocolConfig as InternalProtocolConfig import org.wfanet.measurement.internal.kingdom.ProtocolConfig.NoiseMechanism as InternalNoiseMechanism import org.wfanet.measurement.internal.kingdom.Requisition as InternalRequisition +import org.wfanet.measurement.internal.kingdom.ProtocolConfig import org.wfanet.measurement.system.v1alpha.Computation import org.wfanet.measurement.system.v1alpha.Computation.MpcProtocolConfig.NoiseMechanism import org.wfanet.measurement.system.v1alpha.ComputationKey @@ -391,6 +392,7 @@ fun InternalNoiseMechanism.toSystemNoiseMechanism(): NoiseMechanism { InternalNoiseMechanism.GEOMETRIC -> NoiseMechanism.GEOMETRIC InternalNoiseMechanism.DISCRETE_GAUSSIAN -> NoiseMechanism.DISCRETE_GAUSSIAN InternalNoiseMechanism.NOISE_MECHANISM_UNSPECIFIED, + ProtocolConfig.NoiseMechanism.NONE, InternalNoiseMechanism.UNRECOGNIZED -> error("invalid internal noise mechanism.") } } diff --git a/src/main/kotlin/org/wfanet/measurement/loadtest/dataprovider/BUILD.bazel b/src/main/kotlin/org/wfanet/measurement/loadtest/dataprovider/BUILD.bazel index 1d251cc461e..dc85fcc128c 100644 --- a/src/main/kotlin/org/wfanet/measurement/loadtest/dataprovider/BUILD.bazel +++ b/src/main/kotlin/org/wfanet/measurement/loadtest/dataprovider/BUILD.bazel @@ -135,6 +135,7 @@ kt_jvm_library( "//src/main/proto/wfa/any_sketch:sketch_kt_jvm_proto", "//src/main/proto/wfa/measurement/api/v2alpha:certificates_service_kt_jvm_grpc_proto", "//src/main/proto/wfa/measurement/api/v2alpha:crypto_kt_jvm_proto", + "//src/main/proto/wfa/measurement/api/v2alpha:direct_computation_kt_jvm_proto", "//src/main/proto/wfa/measurement/api/v2alpha:event_group_kt_jvm_proto", "//src/main/proto/wfa/measurement/api/v2alpha:event_group_metadata_descriptors_service_kt_jvm_grpc_proto", "//src/main/proto/wfa/measurement/api/v2alpha:event_groups_service_kt_jvm_grpc_proto", diff --git a/src/main/kotlin/org/wfanet/measurement/loadtest/dataprovider/EdpSimulator.kt b/src/main/kotlin/org/wfanet/measurement/loadtest/dataprovider/EdpSimulator.kt index 9cd56e6b414..904376dd09f 100644 --- a/src/main/kotlin/org/wfanet/measurement/loadtest/dataprovider/EdpSimulator.kt +++ b/src/main/kotlin/org/wfanet/measurement/loadtest/dataprovider/EdpSimulator.kt @@ -78,6 +78,8 @@ import org.wfanet.measurement.api.v2alpha.SignedData import org.wfanet.measurement.api.v2alpha.copy import org.wfanet.measurement.api.v2alpha.createEventGroupMetadataDescriptorRequest import org.wfanet.measurement.api.v2alpha.createEventGroupRequest +import org.wfanet.measurement.api.v2alpha.deterministicCountDistinct +import org.wfanet.measurement.api.v2alpha.deterministicDistribution import org.wfanet.measurement.api.v2alpha.eventGroup import org.wfanet.measurement.api.v2alpha.eventGroupMetadataDescriptor import org.wfanet.measurement.api.v2alpha.event_templates.testing.TestEvent @@ -915,6 +917,13 @@ class EdpSimulator( measurementSpec: MeasurementSpec, sampledVids: Iterable, ): Measurement.Result { + val noiseMechanism = + when (directNoiseMechanism) { + DirectNoiseMechanism.NONE -> ProtocolConfig.NoiseMechanism.NONE + DirectNoiseMechanism.LAPLACE -> ProtocolConfig.NoiseMechanism.GEOMETRIC + DirectNoiseMechanism.GAUSSIAN -> ProtocolConfig.NoiseMechanism.DISCRETE_GAUSSIAN + } + @Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") // Protobuf enum fields cannot be null. return when (measurementSpec.measurementTypeCase) { MeasurementSpec.MeasurementTypeCase.REACH_AND_FREQUENCY -> { @@ -938,9 +947,15 @@ class EdpSimulator( (sampledNoisedReachValue / measurementSpec.vidSamplingInterval.width).toLong() MeasurementKt.result { - reach = reach { value = scaledNoisedReachValue } + reach = reach { + value = scaledNoisedReachValue + this.noiseMechanism = noiseMechanism + deterministicCountDistinct = deterministicCountDistinct {} + } frequency = frequency { relativeFrequencyDistribution.putAll(noisedFrequencyMap.mapKeys { it.key.toLong() }) + this.noiseMechanism = noiseMechanism + deterministicDistribution = deterministicDistribution {} } } } @@ -956,7 +971,13 @@ class EdpSimulator( val scaledNoisedReachValue = (sampledNoisedReachValue / measurementSpec.vidSamplingInterval.width).toLong() - MeasurementKt.result { reach = reach { value = scaledNoisedReachValue } } + MeasurementKt.result { + reach = reach { + value = scaledNoisedReachValue + this.noiseMechanism = noiseMechanism + deterministicCountDistinct = deterministicCountDistinct {} + } + } } MeasurementSpec.MeasurementTypeCase.MEASUREMENTTYPE_NOT_SET -> { error("Measurement type not set.") @@ -975,6 +996,9 @@ class EdpSimulator( // Use externalDataProviderId since it's a known value the FrontendSimulator can verify. // TODO: Calculate impression from data. value = apiIdToExternalId(DataProviderKey.fromName(edpData.name)!!.dataProviderId) + noiseMechanism = ProtocolConfig.NoiseMechanism.NONE + // TODO(@riemanli): specify impression computation methodology once the real impression + // calculation is done. } } @@ -993,6 +1017,9 @@ class EdpSimulator( // Use externalDataProviderId since it's a known value the FrontendSimulator can verify. seconds = apiIdToExternalId(DataProviderKey.fromName(edpData.name)!!.dataProviderId) } + noiseMechanism = ProtocolConfig.NoiseMechanism.NONE + // TODO(@riemanli): specify duration computation methodology once the real duration + // calculation is done. } } diff --git a/src/main/proto/wfa/measurement/api/v2alpha/BUILD.bazel b/src/main/proto/wfa/measurement/api/v2alpha/BUILD.bazel index 877f6b1364d..fd7d93b6be6 100644 --- a/src/main/proto/wfa/measurement/api/v2alpha/BUILD.bazel +++ b/src/main/proto/wfa/measurement/api/v2alpha/BUILD.bazel @@ -694,3 +694,18 @@ kt_jvm_proto_library( ], deps = [":date_interval_java_proto"], ) + +java_proto_library( + name = "direct_computation_java_proto", + deps = [ + "@wfa_measurement_proto//src/main/proto/wfa/measurement/api/v2alpha:direct_computation_proto", + ], +) + +kt_jvm_proto_library( + name = "direct_computation_kt_jvm_proto", + srcs = [ + "@wfa_measurement_proto//src/main/proto/wfa/measurement/api/v2alpha:direct_computation_proto", + ], + deps = [":direct_computation_java_proto"], +) diff --git a/src/main/proto/wfa/measurement/internal/kingdom/BUILD.bazel b/src/main/proto/wfa/measurement/internal/kingdom/BUILD.bazel index 3e80850c74c..514ed11c5b2 100644 --- a/src/main/proto/wfa/measurement/internal/kingdom/BUILD.bazel +++ b/src/main/proto/wfa/measurement/internal/kingdom/BUILD.bazel @@ -135,6 +135,10 @@ proto_and_java_proto_library( name = "differential_privacy", ) +proto_and_java_proto_library( + name = "direct_computation", +) + proto_and_java_proto_library( name = "duchy_protocol_config", deps = [ diff --git a/src/main/proto/wfa/measurement/internal/kingdom/direct_computation.proto b/src/main/proto/wfa/measurement/internal/kingdom/direct_computation.proto new file mode 100644 index 00000000000..c02b36cf198 --- /dev/null +++ b/src/main/proto/wfa/measurement/internal/kingdom/direct_computation.proto @@ -0,0 +1,61 @@ +// Copyright 2023 The Cross-Media Measurement Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package wfa.measurement.internal.kingdom; + +option java_package = "org.wfanet.measurement.internal.kingdom"; +option java_multiple_files = true; + +// Parameters used when applying the deterministic count distinct methodology. +message DeterministicCountDistinct {} + +// Parameters used when applying the deterministic distribution methodology. +message DeterministicDistribution {} + +// Parameters used when applying the deterministic count methodology. +message DeterministicCount {} + +// Parameters used when applying the deterministic sum methodology. +message DeterministicSum {} + +// Parameters used when applying the Liquid Legions count distinct methodology. +// +// May only be set when the measurement type is REACH. +// To obtain differentially private result, one should add a DP noise to the +// estimate number of sampled registers instead of the target estimate. +message LiquidLegionsCountDistinct { + // The decay rate of the Liquid Legions sketch. Required. + double decay_rate = 1; + + // The maximum size of the Liquid Legions sketch. Required. + int64 max_size = 2; +} + +// Parameters used when applying the Liquid Legions distribution methodology. +// +// May only be set when the measurement type is REACH_AND_FREQUENCY. +// `Requisition`s using this protocol can be fulfilled by calling +// RequisitionFulfillment/FulfillRequisition with an encrypted sketch. +message LiquidLegionsDistribution { + // The decay rate of the Liquid Legions sketch. Required. + double decay_rate = 1; + + // The maximum size of the Liquid Legions sketch. Required. + int64 max_size = 2; + + // The size of the distribution of the sampling indicator value. Required. + int64 sampling_indicator_size = 3; +} diff --git a/src/main/proto/wfa/measurement/internal/kingdom/protocol_config.proto b/src/main/proto/wfa/measurement/internal/kingdom/protocol_config.proto index d8f447eeb4e..7e0fc17deda 100644 --- a/src/main/proto/wfa/measurement/internal/kingdom/protocol_config.proto +++ b/src/main/proto/wfa/measurement/internal/kingdom/protocol_config.proto @@ -32,10 +32,73 @@ message ProtocolConfig { // The mechanism used to generate noise in computations. enum NoiseMechanism { NOISE_MECHANISM_UNSPECIFIED = 0; + NONE = 3; GEOMETRIC = 1; DISCRETE_GAUSSIAN = 2; } + // Configuration for the Direct protocol. + // + // The `DataProvider` may choose from the specified noise mechanisms and + // methodologies. + message Direct { + // Configuration parameters for the deterministic count distinct + // methodology. + message DeterministicCountDistinct {} + // Configuration parameters for the deterministic distribution methodology. + message DeterministicDistribution { + // The maximum frequency to reveal in the distribution. + int32 maximum_frequency = 1; + } + // Configuration parameters for the deterministic count methodology. + message DeterministicCount {} + // Configuration parameters for the deterministic sum methodology. + message DeterministicSum {} + // Configuration parameters for the direct Liquid Legions distribution + // methodology. + message LiquidLegionsDistribution { + // The maximum frequency to reveal in the distribution. + int32 maximum_frequency = 1; + } + // Configuration parameters for the direct Liquid Legions count distinct + // methodology. + message LiquidLegionsCountDistinct {} + + // The set of mechanisms that can be used to generate noise during + // computation. + repeated NoiseMechanism noise_mechanisms = 1; + + // Deterministic count distinct methodology. + // + // Can be used in reach computations. + DeterministicCountDistinct deterministic_count_distinct = 2; + + // Deterministic distribution methodology. + // + // Can be used in frequency computations. + DeterministicDistribution deterministic_distribution = 3; + + // Deterministic count methodology. + // + // Can be used in impression computations. + DeterministicCount deterministic_count = 4; + + // Deterministic sum methodology. + // + // Can be used in watch duration computations. + DeterministicSum deterministic_sum = 5; + + // Liquid Legions count distinct methodology. + // + // Can be used in reach computations. + LiquidLegionsCountDistinct liquid_legions_count_distinct = 6; + + // Liquid Legions distribution methodology. + // + // Can be used in frequency computations. + LiquidLegionsDistribution liquid_legions_distribution = 7; + } + // Configuration for Liquid Legions v2 protocols. message LiquidLegionsV2 { // Parameters for sketch. @@ -72,6 +135,9 @@ message ProtocolConfig { // // Must only be set when the measurement type is REACH. LiquidLegionsV2 reach_only_liquid_legions_v2 = 4; + + // Direct protocol. + Direct direct = 5; } } diff --git a/src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/MeasurementsServiceTest.kt b/src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/MeasurementsServiceTest.kt index 9990862e636..5d48b0f4e2f 100644 --- a/src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/MeasurementsServiceTest.kt +++ b/src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/MeasurementsServiceTest.kt @@ -170,6 +170,7 @@ class MeasurementsServiceTest { service = MeasurementsService( MeasurementsGrpcKt.MeasurementsCoroutineStub(grpcTestServerRule.channel), + NOISE_MECHANISMS ) } @@ -1670,6 +1671,13 @@ class MeasurementsServiceTest { ) } + private val NOISE_MECHANISMS = + listOf( + ProtocolConfig.NoiseMechanism.NONE, + ProtocolConfig.NoiseMechanism.GEOMETRIC, + ProtocolConfig.NoiseMechanism.DISCRETE_GAUSSIAN, + ) + private val DIFFERENTIAL_PRIVACY_PARAMS = differentialPrivacyParams { epsilon = 1.0 delta = 1.0 diff --git a/src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/RequisitionsServiceTest.kt b/src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/RequisitionsServiceTest.kt index cdbf309faed..6a8537ad642 100644 --- a/src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/RequisitionsServiceTest.kt +++ b/src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/RequisitionsServiceTest.kt @@ -84,6 +84,7 @@ import org.wfanet.measurement.internal.kingdom.ComputationParticipantKt.liquidLe import org.wfanet.measurement.internal.kingdom.FulfillRequisitionRequestKt.directRequisitionParams import org.wfanet.measurement.internal.kingdom.Measurement as InternalMeasurement import org.wfanet.measurement.internal.kingdom.ProtocolConfig as InternalProtocolConfig +import org.wfanet.measurement.internal.kingdom.ProtocolConfigKt as InternalProtocolConfigKt import org.wfanet.measurement.internal.kingdom.Requisition as InternalRequisition import org.wfanet.measurement.internal.kingdom.Requisition.Refusal as InternalRefusal import org.wfanet.measurement.internal.kingdom.Requisition.State as InternalState @@ -209,6 +210,65 @@ class RequisitionsServiceTest { assertThat(result).ignoringRepeatedFieldOrder().isEqualTo(expected) } + @Test + fun `listRequisitions requests internal Requisitions with direct protocol`() { + val internalRequisition = + INTERNAL_REQUISITION.copy { + parentMeasurement = + parentMeasurement.copy { + protocolConfig = internalProtocolConfig { + externalProtocolConfigId = "direct" + direct = internalDirectProtocolConfig + } + } + } + + val requisition = + REQUISITION.copy { + protocolConfig = + protocolConfig.copy { + protocols.clear() + protocols += ProtocolConfigKt.protocol { direct = directProtocolConfig } + } + } + + whenever(internalRequisitionMock.streamRequisitions(any())) + .thenReturn(flowOf(internalRequisition, internalRequisition)) + + val request = listRequisitionsRequest { parent = MEASUREMENT_NAME } + + val result = + withMeasurementConsumerPrincipal(MEASUREMENT_CONSUMER_NAME) { + runBlocking { service.listRequisitions(request) } + } + + val expected = listRequisitionsResponse { + requisitions += requisition + requisitions += requisition + } + + val streamRequisitionRequest = + captureFirst { + verify(internalRequisitionMock).streamRequisitions(capture()) + } + + assertThat(streamRequisitionRequest) + .ignoringRepeatedFieldOrder() + .isEqualTo( + streamRequisitionsRequest { + limit = DEFAULT_LIMIT + 1 + filter = + StreamRequisitionsRequestKt.filter { + externalMeasurementConsumerId = EXTERNAL_MEASUREMENT_CONSUMER_ID + externalMeasurementId = EXTERNAL_MEASUREMENT_ID + states += VISIBLE_REQUISITION_STATES + } + } + ) + + assertThat(result).ignoringRepeatedFieldOrder().isEqualTo(expected) + } + @Test fun `listRequisitions with page token returns next page`() { whenever(internalRequisitionMock.streamRequisitions(any())) @@ -619,43 +679,97 @@ class RequisitionsServiceTest { } @Test - fun `fulfillDirectRequisition fulfills the requisition`() = runBlocking { - whenever(internalRequisitionMock.fulfillRequisition(any())) - .thenReturn( - INTERNAL_REQUISITION.copy { - state = InternalState.FULFILLED - details = details { encryptedData = REQUISITION_ENCRYPTED_DATA } + fun `fulfillDirectRequisition fulfills the requisition when direct protocol config is not specified`() = + runBlocking { + whenever(internalRequisitionMock.fulfillRequisition(any())) + .thenReturn( + INTERNAL_REQUISITION.copy { + state = InternalState.FULFILLED + details = details { encryptedData = REQUISITION_ENCRYPTED_DATA } + } + ) + + val request = fulfillDirectRequisitionRequest { + name = REQUISITION_NAME + encryptedData = REQUISITION_ENCRYPTED_DATA + nonce = NONCE + } + + val result = + withDataProviderPrincipal(DATA_PROVIDER_NAME) { + runBlocking { service.fulfillDirectRequisition(request) } } - ) - val request = fulfillDirectRequisitionRequest { - name = REQUISITION_NAME - encryptedData = REQUISITION_ENCRYPTED_DATA - nonce = NONCE + val expected = fulfillDirectRequisitionResponse { state = State.FULFILLED } + + verifyProtoArgument( + internalRequisitionMock, + RequisitionsCoroutineImplBase::fulfillRequisition + ) + .comparingExpectedFieldsOnly() + .isEqualTo( + internalFulfillRequisitionRequest { + externalRequisitionId = EXTERNAL_REQUISITION_ID + nonce = NONCE + directParams = directRequisitionParams { + externalDataProviderId = EXTERNAL_DATA_PROVIDER_ID + encryptedData = REQUISITION_ENCRYPTED_DATA + } + } + ) + + assertThat(result).ignoringRepeatedFieldOrder().isEqualTo(expected) } - val result = - withDataProviderPrincipal(DATA_PROVIDER_NAME) { - runBlocking { service.fulfillDirectRequisition(request) } + @Test + fun `fulfillDirectRequisition fulfills the requisition when direct protocol config is specified`() = + runBlocking { + whenever(internalRequisitionMock.fulfillRequisition(any())) + .thenReturn( + INTERNAL_REQUISITION.copy { + state = InternalState.FULFILLED + details = details { encryptedData = REQUISITION_ENCRYPTED_DATA } + parentMeasurement = + parentMeasurement.copy { + protocolConfig = internalProtocolConfig { + externalProtocolConfigId = "direct" + direct = internalDirectProtocolConfig + } + } + } + ) + + val request = fulfillDirectRequisitionRequest { + name = REQUISITION_NAME + encryptedData = REQUISITION_ENCRYPTED_DATA + nonce = NONCE } - val expected = fulfillDirectRequisitionResponse { state = State.FULFILLED } + val result = + withDataProviderPrincipal(DATA_PROVIDER_NAME) { + runBlocking { service.fulfillDirectRequisition(request) } + } - verifyProtoArgument(internalRequisitionMock, RequisitionsCoroutineImplBase::fulfillRequisition) - .comparingExpectedFieldsOnly() - .isEqualTo( - internalFulfillRequisitionRequest { - externalRequisitionId = EXTERNAL_REQUISITION_ID - nonce = NONCE - directParams = directRequisitionParams { - externalDataProviderId = EXTERNAL_DATA_PROVIDER_ID - encryptedData = REQUISITION_ENCRYPTED_DATA + val expected = fulfillDirectRequisitionResponse { state = State.FULFILLED } + + verifyProtoArgument( + internalRequisitionMock, + RequisitionsCoroutineImplBase::fulfillRequisition + ) + .comparingExpectedFieldsOnly() + .isEqualTo( + internalFulfillRequisitionRequest { + externalRequisitionId = EXTERNAL_REQUISITION_ID + nonce = NONCE + directParams = directRequisitionParams { + externalDataProviderId = EXTERNAL_DATA_PROVIDER_ID + encryptedData = REQUISITION_ENCRYPTED_DATA + } } - } - ) + ) - assertThat(result).ignoringRepeatedFieldOrder().isEqualTo(expected) - } + assertThat(result).ignoringRepeatedFieldOrder().isEqualTo(expected) + } @Test fun `fulfillDirectRequisition throw INVALID_ARGUMENT when name is unspecified`() = runBlocking { @@ -737,6 +851,7 @@ class RequisitionsServiceTest { } companion object { + private const val DEFAULT_MAXIMUM_FREQUENCY_DIRECT_DISTRIBUTION = 10 private val MEASUREMENT_SPEC = measurementSpec { measurementPublicKey = UPDATE_TIME.toByteString() reachAndFrequency = @@ -754,6 +869,38 @@ class RequisitionsServiceTest { nonceHashes += ByteString.copyFromUtf8("foo") } + val directProtocolConfig = + ProtocolConfigKt.direct { + noiseMechanisms += ProtocolConfig.NoiseMechanism.GEOMETRIC + noiseMechanisms += ProtocolConfig.NoiseMechanism.DISCRETE_GAUSSIAN + deterministicCountDistinct = ProtocolConfigKt.DirectKt.deterministicCountDistinct {} + liquidLegionsCountDistinct = ProtocolConfigKt.DirectKt.liquidLegionsCountDistinct {} + deterministicDistribution = + ProtocolConfigKt.DirectKt.deterministicDistribution { + maximumFrequency = DEFAULT_MAXIMUM_FREQUENCY_DIRECT_DISTRIBUTION + } + liquidLegionsDistribution = + ProtocolConfigKt.DirectKt.liquidLegionsDistribution { + maximumFrequency = DEFAULT_MAXIMUM_FREQUENCY_DIRECT_DISTRIBUTION + } + } + + val internalDirectProtocolConfig = + InternalProtocolConfigKt.direct { + noiseMechanisms += InternalProtocolConfig.NoiseMechanism.GEOMETRIC + noiseMechanisms += InternalProtocolConfig.NoiseMechanism.DISCRETE_GAUSSIAN + deterministicCountDistinct = InternalProtocolConfigKt.DirectKt.deterministicCountDistinct {} + liquidLegionsCountDistinct = InternalProtocolConfigKt.DirectKt.liquidLegionsCountDistinct {} + deterministicDistribution = + InternalProtocolConfigKt.DirectKt.deterministicDistribution { + maximumFrequency = DEFAULT_MAXIMUM_FREQUENCY_DIRECT_DISTRIBUTION + } + liquidLegionsDistribution = + InternalProtocolConfigKt.DirectKt.liquidLegionsDistribution { + maximumFrequency = DEFAULT_MAXIMUM_FREQUENCY_DIRECT_DISTRIBUTION + } + } + private val INTERNAL_REQUISITION: InternalRequisition = internalRequisition { externalMeasurementConsumerId = EXTERNAL_MEASUREMENT_CONSUMER_ID externalMeasurementId = EXTERNAL_MEASUREMENT_ID diff --git a/src/test/kotlin/org/wfanet/measurement/loadtest/dataprovider/EdpSimulatorTest.kt b/src/test/kotlin/org/wfanet/measurement/loadtest/dataprovider/EdpSimulatorTest.kt index f0849a8c48b..8aa1ccf4a63 100644 --- a/src/test/kotlin/org/wfanet/measurement/loadtest/dataprovider/EdpSimulatorTest.kt +++ b/src/test/kotlin/org/wfanet/measurement/loadtest/dataprovider/EdpSimulatorTest.kt @@ -197,6 +197,7 @@ private val TIME_RANGE = OpenEndTimeRange.fromClosedDateRange(FIRST_EVENT_DATE.. private const val DUCHY_ID = "worker1" private const val RANDOM_SEED: Long = 0 private val DIRECT_NOISE_MECHANISM = DirectNoiseMechanism.LAPLACE +private val CMMS_DIRECT_NOISE_MECHANISM = ProtocolConfig.NoiseMechanism.GEOMETRIC @RunWith(JUnit4::class) class EdpSimulatorTest { @@ -862,6 +863,10 @@ class EdpSimulatorTest { ) val result = Measurement.Result.parseFrom(decryptResult(request.encryptedData, MC_PRIVATE_KEY).data) + assertThat(result.reach.noiseMechanism == CMMS_DIRECT_NOISE_MECHANISM) + assertThat(result.reach.hasDeterministicCountDistinct()) + assertThat(result.frequency.noiseMechanism == CMMS_DIRECT_NOISE_MECHANISM) + assertThat(result.frequency.hasDeterministicDistribution()) assertThat(result).reachValue().isEqualTo(2000L) assertThat(result).frequencyDistribution().isWithin(0.001).of(mapOf(2L to 0.5, 4L to 0.5)) } @@ -905,6 +910,11 @@ class EdpSimulatorTest { ) val result = Measurement.Result.parseFrom(decryptResult(request.encryptedData, MC_PRIVATE_KEY).data) + + assertThat(result.reach.noiseMechanism == CMMS_DIRECT_NOISE_MECHANISM) + assertThat(result.reach.hasDeterministicCountDistinct()) + assertThat(result.frequency.noiseMechanism == CMMS_DIRECT_NOISE_MECHANISM) + assertThat(result.frequency.hasDeterministicDistribution()) assertThat(result).reachValue().isEqualTo(1920) assertThat(result) .frequencyDistribution() @@ -950,6 +960,9 @@ class EdpSimulatorTest { ) val result = Measurement.Result.parseFrom(decryptResult(request.encryptedData, MC_PRIVATE_KEY).data) + + assertThat(result.reach.noiseMechanism == CMMS_DIRECT_NOISE_MECHANISM) + assertThat(result.reach.hasDeterministicCountDistinct()) assertThat(result).reachValue().isEqualTo(2000L) assertThat(result.hasFrequency()).isFalse() } @@ -995,6 +1008,10 @@ class EdpSimulatorTest { ) val result = Measurement.Result.parseFrom(decryptResult(request.encryptedData, MC_PRIVATE_KEY).data) + + + assertThat(result.reach.noiseMechanism == CMMS_DIRECT_NOISE_MECHANISM) + assertThat(result.reach.hasDeterministicCountDistinct()) assertThat(result).reachValue().isEqualTo(1920) assertThat(result.hasFrequency()).isFalse() }