diff --git a/AGGREGATE.md b/AGGREGATE.md
index 9f90ab1538..e91f8b38b7 100644
--- a/AGGREGATE.md
+++ b/AGGREGATE.md
@@ -551,6 +551,56 @@ as when a trigger context ID is set.
See [flexible_filtering.md](https://github.com/patcg-individual-drafts/private-aggregation-api/blob/main/flexible_filtering.md) for more details.
+### Optional: named budgets
+
+Named budgets is an optional feature that gives API callers the ability
+to manage `L1` contribution budget distribution across different types of
+attributions, addressing common challenges such as:
+
+- Allocating the privacy budget between different types of attributions
+ (e.g., biddable vs. non-biddable).
+- Distributing the budget across multiple product categories to prevent any
+ single product category from consuming all available privacy budget.
+
+[Source registrations](#attribution-source-registration) will accept an optional
+field `named_budgets`, which is a dictionary used to set the
+maximum contribution for each named budget for this source.
+
+```jsonc
+{
+ ..., // existing fields
+ "named_budgets": {
+ "budgetName1": 32768, // Max contribution budget for budgetName1.
+ "budgetName2": 32768 // Max contribution budget for budgetName2.
+ }
+}
+```
+
+[Trigger registrations](#attribution-trigger-registration) will accept an
+optional field `named_budgets`, which will be used to select the
+named budget for the generated aggregatable report.
+
+```jsonc
+{
+ ..., // existing fields
+ "named_budgets": [
+ {
+ "name": "budgetName1",
+ "filters": {"source_type": ["navigation"]}
+ }
+ ]
+}
+```
+
+The first named budget from the trigger that matches the source's filter data
+will be selected. If there is no budget name specified or no matching filters, the
+`L1` contribution budget will still be applied.
+
+When generating an aggregatable report, in addition to performing the
+current `L1` budget limit check, the contributions for the report will
+be checked against the available budget with the selected budget name, if applicable.
+If the budget is insufficient, the aggregatable report will not be generated.
+
## Data processing through a Secure Aggregation Service
The exact design of the service is not specified here. We expect to have more
diff --git a/index.bs b/index.bs
index b09f206013..64ad5198a8 100644
--- a/index.bs
+++ b/index.bs
@@ -834,6 +834,12 @@ An attribution source is a [=struct=] with the following items:
non-negative 128-bit integers.
: remaining aggregatable attribution budget
:: A non-negative integer.
+: named budgets
+:: A [=map=] whose [=map/key|keys=] are [=strings=] and whose [=map/value|values=] are
+ non-negative integers.
+: remaining named budgets
+:: A [=map=] whose [=map/key|keys=] are [=strings=] and whose [=map/value|values=] are
+ non-negative integers.
: aggregatable dedup keys
:: A [=set=] of [=aggregatable dedup key/dedup key|aggregatable dedup key values=] associated with this [=attribution source=].
: debug reporting enabled
@@ -918,6 +924,20 @@ An aggregatable dedup key is a [=struct=] with the following items:
+
Named budget
+
+A named budget is a [=struct=] with the following items:
+
+
+: name
+:: Null or a [=string=].
+: filters
+:: A [=list=] of [=filter configs=].
+: negated filters
+:: A [=list=] of [=filter configs=].
+
+
+
Event-level trigger configuration
An event-level trigger configuration is a [=struct=] with the following items:
@@ -996,6 +1016,8 @@ An attribution trigger is a [=struct=] with the following items:
:: An [=aggregatable debug reporting config=].
: attribution scopes
:: A [=set=] of [=strings=].
+: named budgets
+:: A [=list=] of [=named budgets=].
@@ -1185,6 +1207,7 @@ Possible values are:
"trigger-aggregate-deduplicated"
"trigger-aggregate-excessive-reports"
"trigger-aggregate-insufficient-budget"
+
"trigger-aggregate-insufficient-named-budget"
"trigger-aggregate-no-contributions"
"trigger-aggregate-report-window-passed"
"trigger-aggregate-storage-limit"
@@ -1414,6 +1437,18 @@ Its value is 50.
maximum [=set/size=] of an [=attribution source=]'s [=attribution scopes/values=].
Its value is 20.
+Max length per budget name for source is a positive integer that controls
+the maximum [=string/length=] of keys of an [=attribution source=]'s
+[=attribution source/named budgets=] and
+[=attribution source/remaining named budgets=].
+Its value is 25.
+
+Max named budgets per source registration is a positive integer that
+controls the maximum [=map/size=] of an [=attribution source=]'s
+[=attribution source/remaining named budgets=]
+and [=attribution source/named budgets=].
+Its value is 25.
+
# Vendor-Specific Values # {#vendor-specific-values}
Max pending sources per source origin is a positive integer that
@@ -2383,6 +2418,7 @@ A source-registration JSON key is one of the following:
"limit"
"max_event_level_reports"
"max_event_states"
+
"named_budgets"
"priority"
"source_event_id"
"start_time"
@@ -2459,6 +2495,22 @@ To parse aggregation keys given a [=map=] |map|:
1. [=map/Set=] |aggregationKeys|[|key|] to |keyPiece|.
1. Return |aggregationKeys|.
+To parse named budgets for source given a [=map=] |map|:
+
+1. Let |namedBudgets| be a new [=map=].
+1. If |map|["[=source-registration JSON key/named_budgets=]"] does not [=map/exists|exist=], return |namedBudgets|.
+1. Let |values| be |map|["[=source-registration JSON key/named_budgets=]"].
+1. If |values| is not a [=map=], return an error.
+1. If |values|'s [=map/size=] is greater than the
+ [=max named budgets per source registration=], return an error.
+1. [=map/iterate|For each=] |key| → |value| of |values|:
+ 1. If |key|'s [=string/length=] is greater than the [=max length per budget name for source=],
+ return an error.
+ 1. If |value| is not an integer or is less than 0 or is greater than
+ [=allowed aggregatable budget per source=], return an error.
+ 1. [=map/Set=] |namedBudgets|[|key|] to |value|.
+1. Return |namedBudgets|.
+
To obtain default effective windows given a [=source type=] |sourceType|,
a [=moment=] |sourceTime|, and a [=duration=] |eventReportWindow|:
@@ -2785,6 +2837,8 @@ To parse source-registration JSON given a [=byte sequence=]
|value|["[=source-registration JSON key/aggregatable_debug_reporting=]"],
|aggregatableDebugBudget|, and |aggregatableDebugReportingConfig|.
1. Let |aggregatableAttributionBudget| be [=allowed aggregatable budget per source=] - |aggregatableDebugBudget|.
+1. Let |namedBudgets| be the result of [=parsing named budgets for source=] with |value|.
+1. If |namedBudgets| is an error, return it.
1. If [=automation local testing mode=] is true, set |epsilon| to `∞`.
1. Let |source| be a new [=attribution source=] struct whose items are:
@@ -2824,6 +2878,10 @@ To parse source-registration JSON given a [=byte sequence=]
:: |aggregationKeys|
: [=attribution source/remaining aggregatable attribution budget=]
:: |aggregatableAttributionBudget|
+ : [=attribution source/named budgets=]
+ :: |namedBudgets|
+ : [=attribution source/remaining named budgets=]
+ :: |namedBudgets|
: [=attribution source/debug reporting enabled=]
:: |debugReportingEnabled|
: [=attribution source/trigger-data matching mode=]
@@ -3385,6 +3443,8 @@ A trigger-registration JSON key is one of the following:
"filtering_id"
"filters"
"key_piece"
+
"name"
+
"named_budgets"
"not_filters"
"priority"
"source_keys"
@@ -3589,6 +3649,31 @@ To parse aggregatable dedup keys given a [=map=] |map|:
1. [=set/Append=] |aggregatableDedupKey| to |aggregatableDedupKeys|.
1. Return |aggregatableDedupKeys|.
+To parse named budgets for trigger given a [=map=] |map|:
+
+1. Let |namedBudgets| be a new [=list=].
+1. If |map|["[=trigger-registration JSON key/named_budgets=]"] does not [=map/exist=], return |namedBudgets|.
+1. Let |values| be |map|["[=trigger-registration JSON key/named_budgets=]"].
+1. If |values| is not a [=list=], return an error.
+1. [=list/iterate|For each=] |value| of |values|:
+ 1. If |value| is not a [=map=], return an error.
+ 1. Let |name| be null.
+ 1. If |map|["[=trigger-registration JSON key/name=]"] [=map/exists=]:
+ 1. Set |name| to |value|["[=trigger-registration JSON key/name=]"].
+ 1. If |name| is not a [=string=], return an error.
+ 1. Let |filterPair| be the result of running [=parse a filter pair=] with
+ |value|.
+ 1. If |filterPair| is an error, return it.
+ 1. Let |namedBudget| be a new [=named budget=] with the items:
+ : [=named budget/name=]
+ :: |name|
+ : [=named budget/filters=]
+ :: |filterPair|[0]
+ : [=named budget/negated filters=]
+ :: |filterPair|[1]
+ 1. [=list/Append=] |namedBudget| to |namedBudgets|.
+1. Return |namedBudgets|.
+
To parse attribution scopes for trigger from a [=map=] |map|:
1. Let |result| be a new [=set=].
1. If |map|["[=trigger-registration JSON key/attribution_scopes=]"] does not [=map/exist=], return |result|.
@@ -3620,6 +3705,9 @@ a [=moment=] |triggerTime|, and a [=boolean=] |fenced|:
1. Let |aggregatableDedupKeys| be the result of running [=parse aggregatable dedup keys=]
with |value|.
1. If |aggregatableDedupKeys| is an error, return it.
+1. Let |namedBudgets| be the result of running [=parse named budgets for trigger=]
+ with |value|.
+1. If |namedBudgets| is an error, return it.
1. Let |debugKey| be the result of running
[=parse an optional 64-bit unsigned integer=] with |value|, "[=trigger-registration JSON key/debug_key=]",
and null.
@@ -3860,6 +3948,24 @@ To check if an [=attribution source=] can create [=aggregatable contributio
|remainingAggregatableBudget|, return false.
1. Return true.
+To find matching budget name given an [=attribution trigger=] |trigger| and an [=attribution source=] |sourceToAttribute|:
+1. [=list/iterate|For each=] [=named budget=] |namedBudget| of |trigger|'s [=attribution trigger/named budgets=]:
+ 1. If the result of running [=match an attribution source against filters and negated filters=]
+ with |sourceToAttribute|, |namedBudget|'s [=named budget/filters=],
+ |namedBudget|'s [=named budget/negated filters=], and
+ |trigger|'s [=attribution trigger/trigger time=] is true:
+ 1. Return |namedBudget|'s [=named budget/name=].
+1. Return null.
+
+To check if an [=attribution source=] can create [=aggregatable contributions=] for matched budget name
+given an [=aggregatable attribution report=] |report|, an [=attribution source=] |sourceToAttribute|,
+and a [=string=] |matchedBudgetName|, run the following steps:
+
+1. If |sourceToAttribute|'s [=attribution source/remaining named budgets=][|matchedBudgetName|] does not [=map/exists|exist=], return true.
+1. If |report|'s [=aggregatable attribution report/required aggregatable budget=] is greater than
+ |sourceToAttribute|'s [=attribution source/remaining named budgets=][|matchedBudgetName|], return false.
+1. Return true.
+
Obtaining verbose debug data on trigger registration
To obtain verbose debug data body on trigger registration given a
@@ -3886,6 +3992,14 @@ a possibly null [=attribution source=] |sourceToAttribute|, and a possibly null
: "[=trigger debug data type/trigger-aggregate-insufficient-budget=]"
:: [=map/Set=] |body|["`limit`"] to [=allowed aggregatable budget per source=],
[=serialize an integer|serialized=].
+ : "[=trigger debug data type/trigger-aggregate-insufficient-named-budget=]"
+ ::
+ 1. [=Assert=]: |sourceToAttribute| is not null.
+ 1. Let |matchedBudgetName| be the result of running [=find matching budget name=] with |trigger| and |sourceToAttribute|.
+ 1. [=Assert=]: |matchedBudgetName| is not null and |sourceToAttribute|'s [=attribution source/named budgets=][|matchedBudgetName|] [=map/exists=].
+ 1. [=map/Set=] |body|["`name`"] to |matchedBudgetName|.
+ 1. [=map/Set=] |body|["`limit`"] to |sourceToAttribute|'s [=attribution source/named budgets=][|matchedBudgetName|],
+ [=serialize an integer|serialized=].
: "[=trigger debug data type/trigger-aggregate-excessive-reports=]"
:: [=map/Set=] |body|["`limit`"] to [=max aggregatable reports per source=][0],
: "[=trigger debug data type/trigger-event-low-priority=]"
@@ -4156,10 +4270,18 @@ To trigger aggregatable attribution given an [=attribution trigger=]
with |report| and |sourceToAttribute| is false:
1. Return the [=triggering result=] ("[=triggering status/dropped=]",
("[=trigger debug data type/trigger-aggregate-insufficient-budget=]", null)).
+1. Let |matchedBudgetName| be the result of running [=find matching budget name=] with |trigger| and |sourceToAttribute|.
+1. If |matchedBudgetName| is not null and the result of running [=check if an attribution source can create aggregatable contributions for matched budget name=]
+ with |report|, |sourceToAttribute|, and |matchedBudgetName| is false:
+ 1. Return the [=triggering result=] ("[=triggering status/dropped=]",
+ ("[=trigger debug data type/trigger-aggregate-insufficient-named-budget=]", null)).
1. [=set/Append=] |report| to the [=aggregatable attribution report cache=].
1. Increment |sourceToAttribute|'s [=attribution source/number of aggregatable attribution reports=] value by 1.
1. Decrement |sourceToAttribute|'s [=attribution source/remaining aggregatable attribution budget=] value by
|report|'s [=aggregatable attribution report/required aggregatable budget=].
+1. If |matchedBudgetName| is not null and |sourceToAttribute|'s [=attribution source/remaining named budgets=][|matchedBudgetName|] [=map/exists=]:
+ 1. Decrement |sourceToAttribute|'s [=attribution source/remaining named budgets=][|matchedBudgetName|] value by
+ |report|'s [=aggregatable attribution report/required aggregatable budget=].
1. If |matchedDedupKey| is not null, [=list/append=] it to |sourceToAttribute|'s [=attribution source/aggregatable dedup keys=].
1. [=set/Append=] |rateLimitRecord| to the [=attribution rate-limit cache=].
1. Run [=generate null attribution reports=] with |trigger| and |report|.
diff --git a/ts/src/constants.ts b/ts/src/constants.ts
index 9aab339bbe..e906dad64d 100644
--- a/ts/src/constants.ts
+++ b/ts/src/constants.ts
@@ -84,6 +84,7 @@ export const triggerAggregatableDebugTypes: Readonly<[string, ...string[]]> = [
'trigger-aggregate-excessive-reports',
'trigger-aggregate-no-contributions',
'trigger-aggregate-insufficient-budget',
+ 'trigger-aggregate-insufficient-named-budget',
'trigger-aggregate-storage-limit',
'trigger-aggregate-report-window-passed',
'trigger-event-attributions-per-source-destination-limit',
@@ -104,3 +105,7 @@ export const triggerAggregatableDebugTypes: Readonly<[string, ...string[]]> = [
]
export const defaultMaxEventStates: number = 3
+
+export const maxNamedBudgetsPerSource: number = 25
+
+export const maxLengthPerBudgetName: number = 25
diff --git a/ts/src/header-validator/index.html b/ts/src/header-validator/index.html
index 8494790117..1fa1c8e461 100644
--- a/ts/src/header-validator/index.html
+++ b/ts/src/header-validator/index.html
@@ -85,6 +85,7 @@