From 7a4c50415b65d4e75693fb80288bfcc18fdbb980 Mon Sep 17 00:00:00 2001 From: "Benjamin M. Case" <35273659+bmcase@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:33:58 +0900 Subject: [PATCH 1/7] Update api.bs --- api.bs | 360 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 348 insertions(+), 12 deletions(-) diff --git a/api.bs b/api.bs index 3e8b23f..d0977f2 100644 --- a/api.bs +++ b/api.bs @@ -1068,14 +1068,13 @@ For example, "`extra.example.com`" is parsed as "`example.com`". ## State For Privacy Budget Management ## {#privacy-state} -[=User agents=] maintain three pieces of state +[=User agents=] maintain several pieces of state that are used to manage the expenditure of [=privacy budgets=]: * The [=privacy budget store=] records the state of the per-[=site=] and per-[=epoch=] [=privacy budgets=]. It is updated by [=deduct privacy budget=]. - * The [=epoch start store=] records when each [=epoch=] starts for [=conversion sites=]. This store is initialized as a side effect @@ -1084,19 +1083,26 @@ that are used to manage the expenditure of [=privacy budgets=]: * A singleton [=last browsing history clear=] value that tracks when the browsing activity for a [=site=] was last cleared. +* The [=global privacy budget store=] records the state + of the per-[=epoch=] global [=privacy budget=] + that applies across all [=sites=]. + +* The [=impression site quota store=] records the state + of per-[=impression site=] and per-[=epoch=] quota [=privacy budgets=]. + +* The [=conversion site quota store=] records the state + of per-[=conversion site=] and per-[=epoch=] quota [=privacy budgets=]. + +* The [=user action context store=] records which [=sites=] + have accessed quota [=privacy budgets=] within the current [=user action context=]. +
Like the [=impression store=], -the [=privacy budget store=] does not use a [=storage key=]. +the [=privacy budget store=] and related stores do not use a [=storage key=]. These stores have some additional constraints on how information is cleared; see [[#clear-budget-store]] for details. -
-The [=safety limits=] need to be described in more detail. -Some references to clearing -the [=impression store=] may need to be -updated to refer to the [=privacy budget store=] as well. - ### Privacy Budget Store ### {#s-privacy-budget-store} @@ -1173,6 +1179,218 @@ is added to the aggregated histogram. +
This check enforces the quota-count cap (kquota-count) + from the Big Bird algorithm, which limits how many distinct sites + can create new quota budgets within a single user action. + +1. Let |globalBudget| be the [=map/get|value=] of |epoch| + in the [=global privacy budget store=], + or the [=global budget per epoch=] if not present. + +1. If |deduction| is greater than |globalBudget|, return false. + +1. [=set/iterate|For each=] |impressionSite| in |impressionSites|: + + 1. Let |impressionKey| be an [=impression site quota key=] + with |epoch| and |impressionSite|. + + 1. Let |impressionQuota| be the [=map/get|value=] of |impressionKey| + in the [=impression site quota store=], + or the [=impression site quota per epoch=] if not present. + + 1. If |deduction| is greater than |impressionQuota|, return false. + +1. Let |conversionKey| be a [=conversion site quota key=] + with |epoch| and |conversionSite|. + +1. Let |conversionQuota| be the [=map/get|value=] of |conversionKey| + in the [=conversion site quota store=], + or the [=conversion site quota per epoch=] if not present. + +1. If |deduction| is greater than |conversionQuota|, return false. + +
The above steps implement the "Check" phase of the atomic two-phase commit protocol + from the Big Bird algorithm. All budget checks (quota-count, global budget, impression-site quotas, + and conversion-site quota) must succeed before any deductions are made. + If any check fails, the transaction aborts and NO budgets are deducted. + +1. [=map/Set=] the [=global privacy budget store=]\[|epoch|] + to |globalBudget| − |deduction|. + +1. [=set/iterate|For each=] |impressionSite| in |impressionSites|: + + 1. Let |impressionKey| be an [=impression site quota key=] + with |epoch| and |impressionSite|. + + 1. Let |impressionQuota| be the [=map/get|value=] of |impressionKey| + in the [=impression site quota store=], + or the [=impression site quota per epoch=] if not present. + + 1. [=map/Set=] the [=impression site quota store=]\[|impressionKey|] + to |impressionQuota| − |deduction|. + +1. [=map/Set=] the [=conversion site quota store=]\[|conversionKey|] + to |conversionQuota| − |deduction|. + +
The above steps implement the "Consume" phase of the atomic transaction. + Since all checks passed, we now commit by deducting from all relevant budgets. + +1. [=set/iterate|For each=] |impressionSite| in |impressionSites|: + + 1. [=set/Append=] |impressionSite| to |accessedSites|. + +1. [=set/Append=] |conversionSite| to |accessedSites|. + +1. [=map/Set=] the [=user action context store=]\[|uaContext|] + to |accessedSites|. + +1. Return true. + +
Unlike the per-[=site=] [=privacy budget store=], +the [=global privacy budget store=] is keyed only by [=epoch index=], +not by [=site=]. + + +### Impression Site Quota Store ### {#s-impression-site-quota-store} + +The impression site quota store is a [=map=] whose keys are +[=impression site quota keys=] and whose values are [=32-bit unsigned integers=] +in units of [=microepsilons=]. + +An impression site quota key is a [=tuple=] consisting of the following items: + +
A [=user action context=] typically corresponds to +a top-level navigation or other substantial user interaction. +[=User agents=] determine when a new [=user action context=] begins +based on their understanding of intentional user actions. + +
The [=user agent=] determines when [=user action contexts=] expire +and are removed from the [=user action context store=]. +Contexts typically expire after some period of inactivity +or when a new top-level navigation occurs. + +
Typical values might be: +εglobal = 10 × εconv-quota, +εconv-quota = 1.5 × εquerier, +εimp-quota = n × 1.5 × εquerier, +where εquerier is a typical per-site budget (e.g., 1.0) +and n is the expected number of [=conversion sites=] +that query a single [=impression site=] (e.g., 3-5). + + ### Last Browsing History Clear Time ### {#last-clear} The last browsing history clear is a [=moment=] @@ -1367,6 +1617,10 @@ and a [=moment=] |now|: 1. [=map/clear|Clear=] the [=epoch start store=]. +
TODO: Define how to clear [=safety limits=] stores: + [=global privacy budget store=], [=impression site quota store=], + [=conversion site quota store=], and [=user action context store=]. + 1. If |sites| [=set/is empty|is not empty=]: 1. [=set/iterate|For each=] |impression| in the [=impression store=], @@ -1438,6 +1692,26 @@ The saveImpression(|options|) method steps are 1. If any result in |conversionCallers| is failure, return [=a promise rejected with=] a {{"SyntaxError"}} {{DOMException}} in |realm|. 1. Run the following steps [=in parallel=]: + 1. Let |uaContext| be the [=current user action context=]. + + 1. Let |accessedSites| be the [=map/get|value=] of |uaContext| + in the [=user action context store=], + or an empty [=set=] if not present. + + 1. If |accessedSites| does not [=set/contain=] |site|: + + 1. If the [=set/size=] of |accessedSites| is greater than or equal to + the [=quota count cap=], return. + +
This implements the quota-count cap check from the Big Bird algorithm, + preventing a single user action from creating new impression-site quotas + for more than [=quota count cap=] distinct sites. + + 1. [=set/Append=] |site| to |accessedSites|. + + 1. [=map/Set=] the [=user action context store=]\[|uaContext|] + to |accessedSites|. + 1. Construct |impression| as a [=impression|saved impression=] comprising: : [=impression/Match Value=] :: |options|.{{AttributionImpressionOptions/matchValue}} @@ -1459,6 +1733,10 @@ The saveImpression(|options|) method steps are :: |options|.{{AttributionImpressionOptions/priority}} 1. If the Attribution API is [[#opt-out|enabled]], save |impression| to the [=impression store=]. + +
Impressions are stored with their timestamp, + which is later used to determine which [=epoch=] they belong to + when matching during conversion measurement. 1. Let |result| be a new {{AttributionImpressionResult}}. 1. Return [=a promise resolved with=] |result| in |realm|. @@ -1496,6 +1774,10 @@ The measureConversion(|options|) method steps 1. otherwise, the result of [=obtain a site|obtaining a site=] from |settings|' [=environment settings object/origin=]. + 1. Let |uaContext| be the [=current user action context=]. + +
The [=user agent=] determines when a new [=user action context=] begins, + typically corresponding to a top-level navigation or other substantial user interaction. 1. Let |validatedOptions| be the result of [=validate AttributionConversionOptions|validating=] |options|, returning [=a promise rejected with=] any thrown reason. @@ -1505,7 +1787,7 @@ The measureConversion(|options|) method steps |validatedOptions|' [=validated conversion options/histogram size=]. 1. If the Attribution API is [[#opt-out|enabled]], set |report| to the result of [=do attribution and fill a histogram=] with |validatedOptions|, - |topLevelSite|, |intermediarySite|, and |now|. + |topLevelSite|, |intermediarySite|, |uaContext|, and |now|. 1. Let |aggregationService| be |validatedOptions|'s [=validated conversion options/aggregation service=]. 1. Switch on the value of |aggregationService|.{{AttributionAggregationService/protocol}}:
When attributing across multiple [=epochs=], + each [=epoch=]'s [=impressions=] are evaluated separately. + [=Impressions=] are organized by [=epoch=] based on their [=impression/timestamp=] + relative to the [=conversion site=]. + For each [=epoch=], both the per-[=site=] [=privacy budget=] and [=safety limits=] + are checked and deducted independently using an atomic transaction. + This approach ensures that budget consumption is properly tracked across time periods. + 1. For each |epoch| from |startEpoch| to |currentEpoch|, inclusive: 1. Let |impressions| be the result of invoking [=common matching logic=] with |options|, |topLevelSite|, |intermediarySite|, |epoch|, and |now|. +
The [=common matching logic=] filters [=impressions=] + from the [=impression store=] by comparing each impression's [=impression/timestamp=] + against |topLevelSite| to determine which [=epoch=] it belongs to. + Only [=impressions=] that fall within the current |epoch| are selected. + 1. If |impressions| [=set/is empty|is not empty=]: + 1. Let |impressionSites| be an [=set/is empty|empty=] [=set=]. + + 1. [=set/iterate|For each=] |impression| in |impressions|, + [=set/append=] |impression|'s [=impression/impression site=] to |impressionSites|. + 1. Let |key| be a [=privacy budget key=] whose items are |epoch| and |topLevelSite|. 1. Let |budgetOk| be the result of invoking [=deduct privacy budget=] @@ -1664,12 +1965,37 @@ To do attribution and fill a histogram, given |options|'s [=validated conversion options/max value=], and null. - 1. If |budgetOk| is true, [=set/extend=] |matchedImpressions| with |impressions|. + 1. Let |safetyOk| be the result of invoking [=check and deduct safety limit budgets for impression sites=] + with |epoch|, + |impressionSites|, + |topLevelSite|, + |uaContext|, + |options|' [=validated conversion options/epsilon=], + |options|' [=validated conversion options/value=], + |options|'s [=validated conversion options/max value=], + and null. + + 1. If |budgetOk| is true and |safetyOk| is true, [=set/extend=] |matchedImpressions| with |impressions|. + +
If either the per-[=site=] [=privacy budget=] or the [=safety limits=] + for a given [=epoch=] are insufficient, [=impressions=] from that [=epoch=] are excluded + from attribution (by dropping that epoch's data), but [=impressions=] from other [=epochs=] + with sufficient budget may still be included. + This implements the per-epoch atomic transaction pattern from the Big Bird algorithm. 1. If |matchedImpressions| [=set/is empty=], return the result of invoking [=create an all-zero histogram=] with |options|' [=validated conversion options/histogram size=]. +1. Let |impressionSites| be an [=set/is empty|empty=] [=set=]. + +1. [=set/iterate|For each=] |impression| in |matchedImpressions|, + [=set/append=] |impression|'s [=impression/impression site=] to |impressionSites|. + +
The [=common matching logic=] can select [=impressions=] + from multiple [=impression sites=], + such as to support cross-publisher attribution. + 1. Set |histogram| to the result of [=fill a histogram with last-n-touch attribution=] with |matchedImpressions|, |options|' [=validated conversion options/histogram size=], |options|' [=validated conversion options/value=], and @@ -1688,7 +2014,17 @@ To do attribution and fill a histogram, given |options|'s [=validated conversion options/max value=], and |l1Norm|. - 1. If |budgetOk| is false, set |histogram| to the result of invoking + 1. Let |safetyOk| be the result of invoking [=check and deduct safety limit budgets for impression sites=] + with |currentEpoch|, + |impressionSites|, + |topLevelSite|, + |uaContext|, + |options|' [=validated conversion options/epsilon=], + |options|' [=validated conversion options/value=], + |options|'s [=validated conversion options/max value=], + and |l1Norm|. + + 1. If |budgetOk| is false or |safetyOk| is false, set |histogram| to the result of invoking [=create an all-zero histogram=] with |options|' [=validated conversion options/histogram size=]. 1. Return |histogram|. From 3db436805ef4601024d05c8b64a4516d06aac230 Mon Sep 17 00:00:00 2001 From: "Benjamin M. Case" <35273659+bmcase@users.noreply.github.com> Date: Wed, 12 Nov 2025 11:14:33 +0900 Subject: [PATCH 2/7] update safety limit draft --- api.bs | 175 +++------------------------------------------------------ 1 file changed, 9 insertions(+), 166 deletions(-) diff --git a/api.bs b/api.bs index d0977f2..3dc0dc2 100644 --- a/api.bs +++ b/api.bs @@ -1103,6 +1103,11 @@ These stores have some additional constraints on how information is cleared; see [[#clear-budget-store]] for details. +
+Some references to clearing +the [=impression store=] may need to be +updated to refer to the [=privacy budget store=] as well. + ### Privacy Budget Store ### {#s-privacy-budget-store} @@ -1179,111 +1184,7 @@ is added to the aggregated histogram. -
This check enforces the quota-count cap (kquota-count) - from the Big Bird algorithm, which limits how many distinct sites - can create new quota budgets within a single user action. - -1. Let |globalBudget| be the [=map/get|value=] of |epoch| - in the [=global privacy budget store=], - or the [=global budget per epoch=] if not present. - -1. If |deduction| is greater than |globalBudget|, return false. - -1. [=set/iterate|For each=] |impressionSite| in |impressionSites|: - - 1. Let |impressionKey| be an [=impression site quota key=] - with |epoch| and |impressionSite|. - - 1. Let |impressionQuota| be the [=map/get|value=] of |impressionKey| - in the [=impression site quota store=], - or the [=impression site quota per epoch=] if not present. - - 1. If |deduction| is greater than |impressionQuota|, return false. - -1. Let |conversionKey| be a [=conversion site quota key=] - with |epoch| and |conversionSite|. - -1. Let |conversionQuota| be the [=map/get|value=] of |conversionKey| - in the [=conversion site quota store=], - or the [=conversion site quota per epoch=] if not present. -1. If |deduction| is greater than |conversionQuota|, return false. - -
The above steps implement the "Check" phase of the atomic two-phase commit protocol - from the Big Bird algorithm. All budget checks (quota-count, global budget, impression-site quotas, - and conversion-site quota) must succeed before any deductions are made. - If any check fails, the transaction aborts and NO budgets are deducted. - -1. [=map/Set=] the [=global privacy budget store=]\[|epoch|] - to |globalBudget| − |deduction|. - -1. [=set/iterate|For each=] |impressionSite| in |impressionSites|: - - 1. Let |impressionKey| be an [=impression site quota key=] - with |epoch| and |impressionSite|. - - 1. Let |impressionQuota| be the [=map/get|value=] of |impressionKey| - in the [=impression site quota store=], - or the [=impression site quota per epoch=] if not present. - - 1. [=map/Set=] the [=impression site quota store=]\[|impressionKey|] - to |impressionQuota| − |deduction|. - -1. [=map/Set=] the [=conversion site quota store=]\[|conversionKey|] - to |conversionQuota| − |deduction|. - -
The above steps implement the "Consume" phase of the atomic transaction. - Since all checks passed, we now commit by deducting from all relevant budgets. - -1. [=set/iterate|For each=] |impressionSite| in |impressionSites|: - - 1. [=set/Append=] |impressionSite| to |accessedSites|. - -1. [=set/Append=] |conversionSite| to |accessedSites|. - -1. [=map/Set=] the [=user action context store=]\[|uaContext|] - to |accessedSites|. - -1. Return true.
Typical values might be: -εglobal = 10 × εconv-quota, -εconv-quota = 1.5 × εquerier, -εimp-quota = n × 1.5 × εquerier, -where εquerier is a typical per-site budget (e.g., 1.0) -and n is the expected number of [=conversion sites=] -that query a single [=impression site=] (e.g., 3-5). +TODO ### Last Browsing History Clear Time ### {#last-clear} @@ -1692,26 +1588,6 @@ The saveImpression(|options|) method steps are 1. If any result in |conversionCallers| is failure, return [=a promise rejected with=] a {{"SyntaxError"}} {{DOMException}} in |realm|. 1. Run the following steps [=in parallel=]: - 1. Let |uaContext| be the [=current user action context=]. - - 1. Let |accessedSites| be the [=map/get|value=] of |uaContext| - in the [=user action context store=], - or an empty [=set=] if not present. - - 1. If |accessedSites| does not [=set/contain=] |site|: - - 1. If the [=set/size=] of |accessedSites| is greater than or equal to - the [=quota count cap=], return. - -
This implements the quota-count cap check from the Big Bird algorithm, - preventing a single user action from creating new impression-site quotas - for more than [=quota count cap=] distinct sites. - - 1. [=set/Append=] |site| to |accessedSites|. - - 1. [=map/Set=] the [=user action context store=]\[|uaContext|] - to |accessedSites|. - 1. Construct |impression| as a [=impression|saved impression=] comprising: : [=impression/Match Value=] :: |options|.{{AttributionImpressionOptions/matchValue}} @@ -1734,9 +1610,6 @@ The saveImpression(|options|) method steps are 1. If the Attribution API is [[#opt-out|enabled]], save |impression| to the [=impression store=]. -
Impressions are stored with their timestamp, - which is later used to determine which [=epoch=] they belong to - when matching during conversion measurement. 1. Let |result| be a new {{AttributionImpressionResult}}. 1. Return [=a promise resolved with=] |result| in |realm|. @@ -1929,34 +1802,16 @@ To do attribution and fill a histogram, given 1. If |singleEpoch| is true: 1. Set |matchedImpressions| to the result of invoking [=common matching logic=] with |options|, |topLevelSite|, |intermediarySite|, |currentEpoch|, and |now|. + 1. TODO safety limits in single-epoch case. 1. If |singleEpoch| is false: - -
When attributing across multiple [=epochs=], - each [=epoch=]'s [=impressions=] are evaluated separately. - [=Impressions=] are organized by [=epoch=] based on their [=impression/timestamp=] - relative to the [=conversion site=]. - For each [=epoch=], both the per-[=site=] [=privacy budget=] and [=safety limits=] - are checked and deducted independently using an atomic transaction. - This approach ensures that budget consumption is properly tracked across time periods. - 1. For each |epoch| from |startEpoch| to |currentEpoch|, inclusive: 1. Let |impressions| be the result of invoking [=common matching logic=] with |options|, |topLevelSite|, |intermediarySite|, |epoch|, and |now|. -
The [=common matching logic=] filters [=impressions=] - from the [=impression store=] by comparing each impression's [=impression/timestamp=] - against |topLevelSite| to determine which [=epoch=] it belongs to. - Only [=impressions=] that fall within the current |epoch| are selected. - 1. If |impressions| [=set/is empty|is not empty=]: - 1. Let |impressionSites| be an [=set/is empty|empty=] [=set=]. - - 1. [=set/iterate|For each=] |impression| in |impressions|, - [=set/append=] |impression|'s [=impression/impression site=] to |impressionSites|. - 1. Let |key| be a [=privacy budget key=] whose items are |epoch| and |topLevelSite|. 1. Let |budgetOk| be the result of invoking [=deduct privacy budget=] @@ -1977,24 +1832,12 @@ To do attribution and fill a histogram, given 1. If |budgetOk| is true and |safetyOk| is true, [=set/extend=] |matchedImpressions| with |impressions|. -
If either the per-[=site=] [=privacy budget=] or the [=safety limits=] - for a given [=epoch=] are insufficient, [=impressions=] from that [=epoch=] are excluded - from attribution (by dropping that epoch's data), but [=impressions=] from other [=epochs=] - with sufficient budget may still be included. - This implements the per-epoch atomic transaction pattern from the Big Bird algorithm. + 1. If |matchedImpressions| [=set/is empty=], return the result of invoking [=create an all-zero histogram=] with |options|' [=validated conversion options/histogram size=]. -1. Let |impressionSites| be an [=set/is empty|empty=] [=set=]. - -1. [=set/iterate|For each=] |impression| in |matchedImpressions|, - [=set/append=] |impression|'s [=impression/impression site=] to |impressionSites|. - -
The [=common matching logic=] can select [=impressions=] - from multiple [=impression sites=], - such as to support cross-publisher attribution. 1. Set |histogram| to the result of [=fill a histogram with last-n-touch attribution=] with |matchedImpressions|, |options|' [=validated conversion options/histogram size=], From a8c6b8df9fccf127b4f3d82648a09a16d933f666 Mon Sep 17 00:00:00 2001 From: "Benjamin M. Case" <35273659+bmcase@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:22:35 +0900 Subject: [PATCH 3/7] update safety limit draft --- api.bs | 2 -- 1 file changed, 2 deletions(-) diff --git a/api.bs b/api.bs index 3dc0dc2..4b3e115 100644 --- a/api.bs +++ b/api.bs @@ -1186,7 +1186,6 @@ is added to the aggregated histogram. - ### Global Privacy Budget Store ### {#s-global-privacy-budget-store} @@ -1802,7 +1801,6 @@ To do attribution and fill a histogram, given 1. If |singleEpoch| is true: 1. Set |matchedImpressions| to the result of invoking [=common matching logic=] with |options|, |topLevelSite|, |intermediarySite|, |currentEpoch|, and |now|. - 1. TODO safety limits in single-epoch case. 1. If |singleEpoch| is false: 1. For each |epoch| from |startEpoch| to |currentEpoch|, inclusive: From f217ef5505f7ed91eff47f043d1ea1a5959f621c Mon Sep 17 00:00:00 2001 From: "Benjamin M. Case" <35273659+bmcase@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:16:58 -0500 Subject: [PATCH 4/7] add user action checks This adds the checks that need to happen on user action context, following Alg 2 of BigBird; not that it follows the latest version which has conversion check moved within the for loop over epochs. --- api.bs | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 6 deletions(-) diff --git a/api.bs b/api.bs index 4b3e115..1a96d90 100644 --- a/api.bs +++ b/api.bs @@ -1253,19 +1253,44 @@ limiting its ability to rapidly deplete the [=global privacy budget=]. ### User Action Context Store ### {#s-user-action-context-store} The user action context store is a [=map=] keyed by [=user action contexts=] -and containing values that are [=sets=] of [=sites=]. +and containing values that are [=user action context entries=]. A user action context is an identifier for a sequence of API invocations that are associated with a single intentional user action, such as a navigation or click. +A user action context entry is a [=struct=] with the following fields: + +
A [=user action context=] typically corresponds to a top-level navigation or other substantial user interaction. @@ -1280,8 +1305,14 @@ returning a [=user action context=]: associated with the current execution context, return it. 1. Otherwise, create a new [=user action context=] identifier, - add it to the [=user action context store=] with an empty [=set=] value, - and return it. + create a new [=user action context entry=] with: + * [=user action context entry/Allowed Impression Sites=] set to an empty [=set=], + * [=user action context entry/Impression Site Counter=] set to the [=impression site count cap=], + * [=user action context entry/Allowed Conversion Sites=] set to an empty [=map=], + * [=user action context entry/Conversion Site Counters=] set to an empty [=map=], + + add the identifier and entry to the [=user action context store=], + and return the identifier.
The [=user agent=] determines when [=user action contexts=] expire and are removed from the [=user action context store=]. @@ -1290,6 +1321,67 @@ or when a new top-level navigation occurs. +
TODO: The [=compute impression site deductions=] function needs to still be defined. + +1. Check that sufficient budget exists in all relevant stores + before deducting from any of them. + This ensures atomicity: either all deductions succeed, or none occur. + + 1. If |deduction| is greater than |currentValue|, return false. + + 1. If the [=global privacy budget store=] does not [=map/contain=] |epoch|, + [=map/set=] its value to the [=global budget per epoch=]. + + 1. If |deduction| is greater than [=global privacy budget store=]\[|epoch|], + return false. + + 1. Let |convQuotaKey| be a [=conversion site quota key=] + whose items are |epoch| and |conversionSite|. + + 1. If the [=conversion site quota store=] does not [=map/contain=] |convQuotaKey|, + [=map/set=] its value to the [=conversion site quota per epoch=]. -1. [=map/set|Set=] the value of |key| in the [=privacy budget store=] - to |currentValue| − |deduction| - and return true. + 1. If |deduction| is greater than [=conversion site quota store=]\[|convQuotaKey|], + return false. + + 1. [=map/iterate|For each=] |impSite| → |siteDeduction| in |impSiteDeductions|: + + 1. Let |impQuotaKey| be an [=impression site quota key=] + whose items are |epoch| and |impSite|. + + 1. If the [=impression site quota store=] does not [=map/contain=] |impQuotaKey|, + [=map/set=] its value to the [=impression site quota per epoch=]. + + 1. If |siteDeduction| is greater than [=impression site quota store=]\[|impQuotaKey|], + return false. + +1. All budget checks passed; perform the deductions atomically. + + 1. [=map/set|Set=] the value of |key| in the [=privacy budget store=] + to |currentValue| − |deduction|. + + 1. Decrement [=global privacy budget store=]\[|epoch|] by |deduction|. + + 1. Decrement [=conversion site quota store=]\[|convQuotaKey|] by |deduction|. + + 1. [=map/iterate|For each=] |impSite| → |siteDeduction| in |impSiteDeductions|: + + 1. Let |impQuotaKey| be an [=impression site quota key=] + whose items are |epoch| and |impSite|. + + 1. Decrement [=impression site quota store=]\[|impQuotaKey|] by |siteDeduction|. + +1. Return true.