1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
content / browser / interest_group / trusted_signals_cache_impl.h [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CONTENT_BROWSER_INTEREST_GROUP_TRUSTED_SIGNALS_CACHE_IMPL_H_
#define CONTENT_BROWSER_INTEREST_GROUP_TRUSTED_SIGNALS_CACHE_IMPL_H_
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_refptr.h"
#include "base/types/optional_ref.h"
#include "base/unguessable_token.h"
#include "base/values.h"
#include "content/browser/interest_group/trusted_signals_fetcher.h"
#include "content/common/content_export.h"
#include "content/services/auction_worklet/public/mojom/trusted_signals_cache.mojom.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom-forward.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
struct BiddingAndAuctionServerKey;
// Handles caching (not yet implemented) and dispatching of trusted bidding and
// scoring signals requests. Only handles requests to Trusted Execution
// Environments (TEEs), i.e., versions 2+ of the protocol, so does not handle
// legacy bring-your-own-server (BYOS) requests. The browser process makes a
// request and gets a Handle and a partition ID, which can then be used to fetch
// the response through the Mojo auction_worklet::mojom::TrustedSignalsCache
// API provided by the Cache. The Handle and partition ID are provided
// immediately on invocation, but the network request may not be sent out
// immediately.
//
// The values it vends are guaranteed to remain valid at least until the Handle
// they were returned with is destroyed. Having the cache in the browser process
// allows requests to be sent while the Javascript process is still starting up,
// and allows the cache to live beyond the shutdown of the often short-live
// Javascript processes.
//
// Internally, it uses 4 maps:
//
// * `fetches_`, a multimap of pending/live Fetches, with FetchKeys consisting
// of what must be the same to share a fetch. On fetch completion, ownership of
// the response is passed to the corresponding CompressionGroupData(s) and the
// Fetch is deleted. See FetchKey for more details on why this is a multimap
// rather than a map.
//
// * `compression_group_data_map_`, a map of UnguessableTokens
// (`compression_group_tokens`) to CompressionGroupData, which contain the still
// compressed response for a single partition group within a fetch. A
// CompressionGroupData may have one or more partitions, each of which
// corresponds to a single [Bidding|Scoring]CacheEntry. The lifetime of
// CompressionGroupData is scoped to the Handle objects returned by the Cache.
//
// * `bidding_cache_entries_`, a map of BiddingCacheEntries, with
// BiddingCacheKeys consisting of what must be the same to share a Fetch, a
// compression group, and partition within the group. Fields that can be merged
// between requests to share a partitiong (e.g., trusted signals keys) are part
// of entry itself, not the key. This is a map, not a multimap, so if a
// BiddingCacheEntry cannot be reused (with or without modification) to suit the
// needs of an incoming request, the BiddingCacheEntry is deleted, and removed
// from its CompressionGroupData. Destroying a BiddingCacheEntry in this way
// will not destroy the CompressionGroupData, or the CompressionGroupData's
// fetch, if it has one.
//
// * TODO(https://crbug.com/333445540): A map of ScoringCacheEntries much akin
// to the map of BiddingCacheEntries.
//
// Fetches and CacheEntries have pointers to the corresponding
// CompressionGroupData, while the CompressionGroupData owns the corresponding
// values in the other two maps. Deleting a CompressionGroupData removes the
// corresponding values in the two maps. One CompressionGroupData may own
// multiple CacheEntries, but will only own one live/pending Fetch. Ownership of
// a Fetch may be shared by multiple CompressionGroupData objects with matching
// FetchKeys.
//
// Each handed out Handle object will keep its corresponding
// CompressionGroupData alive until the handle is destroyed.
//
// TODO(https://crbug.com/333445540): Add caching support. Right now, entries
// are cached only as long as there's something that owns a Handle, but should
// instead cache for at least a short duration as long as an entry's TTL hasn't
// expired. Holding onto a CompressionGroupData reference, which is refcounted,
// is all that's needed to keep an entry alive.
//
// TODO(https://crbug.com/333445540): May need some sort of rate limit and size
// cap. Currently, this class creates an arbitrary number of downloads, and
// potentially stores an unlimited amount of data in browser process memory.
class CONTENT_EXPORT TrustedSignalsCacheImpl
: public auction_worklet::mojom::TrustedSignalsCache {
public:
enum class SignalsType {
kBidding,
kScoring,
};
// Callback to retrieve a BiddingAndAuctionServerKey for a given coordinator.
// The `callback` parameter may be invoked synchronously or asynchronously,
// and may fail.
using GetCoordinatorKeyCallback = base::RepeatingCallback<void(
const std::optional<url::Origin>& coordinator,
base::OnceCallback<void(
base::expected<BiddingAndAuctionServerKey, std::string>)> callback)>;
// As long as a Handle is alive, any Mojo
// auction_worklet::mojom::TrustedSignalsCache created by invoking
// CreateMojoPipe() can retrieve the response associated with the
// corresponding signals response ID, which will not change for the lifetime
// of the handle. The ID can be used to request a response from the cache at
// any point in time, but the fetch may be made asynchronously, so there's no
// guarantee of a timely response.
//
// Refcounted so that one handle can be reused for all requests with the same
// `compression_group_token`, so when the Handle is destroyed, we know there
// are no Handles that refer to the corresponding entry in the cache, and it
// may be deleted.
//
// Any pending or future requests through a handed out
// auction_worklet::mojom::TrustedSignalsCache pipe for the
// `compression_group_token` associated with a destroyed Handle will be sent
// an error message.
//
// All outstanding Handles must be released before the TrustedSignalsCacheImpl
// may be destroyed.
//
// Currently, the internal CompressionGroupData class is a subclass of this,
// so callers are hanging on to data associated with a compression group
// directly, but that's not a fundamental design requirement of the API.
class Handle : public base::RefCounted<Handle> {
public:
Handle(Handle&) = delete;
Handle& operator=(Handle&) = delete;
// The token that needs to be passed to GetTrustedSignals() to retrieve the
// response through the auction_worklet::mojom::TrustedSignalsCache API.
// Will not change for the lifetime of the handle.
const base::UnguessableToken& compression_group_token() const {
return compression_group_token_;
}
protected:
friend class base::RefCounted<Handle>;
Handle();
virtual ~Handle();
const base::UnguessableToken compression_group_token_{
base::UnguessableToken::Create()};
};
TrustedSignalsCacheImpl(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
GetCoordinatorKeyCallback get_coordinator_key_callback);
~TrustedSignalsCacheImpl() override;
TrustedSignalsCacheImpl(const TrustedSignalsCacheImpl&) = delete;
TrustedSignalsCacheImpl& operator=(const TrustedSignalsCacheImpl&) = delete;
// Creates a TrustedSignalsCache pipe for a bidder script process. It may only
// be used for `signals_type` fetches for `script_origin`, where `origin` is
// origin of the script that will receive those signals (i.e., the seller
// origin or InterestGroup origin, depending on whether these are scoring or
// bidding signals).
mojo::PendingRemote<auction_worklet::mojom::TrustedSignalsCache>
CreateMojoPipe(SignalsType signals_type, const url::Origin& script_origin);
// Requests bidding signals for the specified interest group. Return value is
// a Handle which must be kept alive until the response to the request is no
// longer needed, and which provides a key to identify the response. Also
// returns `partition_id`, which identifies the partition within the
// compression group identified by Handle::compression_group_token() that will
// have the relevant response.
//
// Never starts a network fetch synchronously. Bidder signals are requested
// over the network after a post task.
scoped_refptr<Handle> RequestTrustedBiddingSignals(
const url::Origin& main_frame_origin,
const url::Origin& interest_group_owner,
const std::string& interest_group_name,
blink::mojom::InterestGroup_ExecutionMode execution_mode,
const url::Origin& joining_origin,
const GURL& trusted_signals_url,
const url::Origin& coordinator,
base::optional_ref<const std::vector<std::string>>
trusted_bidding_signals_keys,
base::Value::Dict additional_params,
int& partition_id);
// Requests scoring signals. Return value is a Handle which must be kept alive
// until the response to the request is no longer needed, and which provides a
// key to identify the response. Also returns `partition_id`, which identifies
// the partition within the compression group identified by
// Handle::compression_group_token() that will have the relevant response.
//
// `interest_group_owner` and `joining_origin` are never sent over the wire,
// but are instead both used to determine if different compression groups
// should be used.
//
// Never starts a network fetch synchronously. Scoring signals are requested
// over the network after a post task.
//
// TODO(mmenke): Implement Some way to delay sending the fetch over the wire
// for a certain duration, with some way for an auction to flush requests once
// it knows all signals it needs have been requested. Probably need to either
// add a method to Handle to flush requests, or add an API to flush all
// Fetches matching a passed in {seller origin, joining origin, publisher
// origin} triplet.
scoped_refptr<TrustedSignalsCacheImpl::Handle> RequestTrustedScoringSignals(
const url::Origin& main_frame_origin,
const url::Origin& seller,
const GURL& trusted_signals_url,
const url::Origin& coordinator,
const url::Origin& interest_group_owner,
const url::Origin& joining_origin,
const GURL& render_url,
const std::vector<GURL>& component_render_urls,
base::Value::Dict additional_params,
int& partition_id);
// TrustedSignalsFetcher implementation:
void GetTrustedSignals(
const base::UnguessableToken& compression_group_token,
mojo::PendingRemote<auction_worklet::mojom::TrustedSignalsCacheClient>
client) override;
private:
// Each receiver pipe in `receiver_set_` is restricted to only receive
// scoring/bidding signals for the specific script origin identified by this
// struct.
struct ReceiverRestrictions {
bool operator==(const ReceiverRestrictions& other) const;
SignalsType signals_type;
url::Origin script_origin;
};
// Key used for live or pending requests to a trusted server. Two request with
// the same FetchKey can be merged together, but the requests themselves may
// differ in other fields. Before the network request is started, any request
// with a matching fetch key may be merged into a single request. Once the
// network request is started, however, new requests may only be merged into
// the live request if there's a matching CacheEntry that has already
// requested all information needed for the request.
//
// There may be multiple requests at once with the same FetchKey, in the case
// a network request was started before a new request came in with values that
// do not match any of those in the live fetch.
//
// Combining requests across main frame origins or owners seems potentially
// problematic in terms of cross-origin leaks, so partition on those for now,
// at least.
struct FetchKey {
FetchKey();
// `script_origin` is the origin of the script that will receive the
// response. For bidding signals fetches, it's the interest group owner. For
// scoring signals fetches, it's the seller origin (component or top-level,
// depending on which seller will be receiving the signals).
FetchKey(const url::Origin& main_frame_origin,
SignalsType signals_type,
const url::Origin& script_origin,
const GURL& trusted_signals_url,
const url::Origin& coordinator);
FetchKey(const FetchKey&);
FetchKey(FetchKey&&);
~FetchKey();
FetchKey& operator=(const FetchKey&);
FetchKey& operator=(FetchKey&&);
bool operator<(const FetchKey& other) const;
// Order here matches comparison order in operator<(), and is based on a
// guess on what order will result in the most performant comparisons.
url::Origin script_origin;
SignalsType signals_type;
// The origin of the frame running the auction that needs the signals. This
// could potentially be used to separate compression groups instead of
// fetches, but best to be safe.
url::Origin main_frame_origin;
GURL trusted_signals_url;
url::Origin coordinator;
};
// A pending or live network request. May be for bidding signals or scoring
// signals, but not both.
struct Fetch;
using FetchMap = std::multimap<FetchKey, Fetch>;
// The cached compression group of a trusted signals response, or an error
// message. May be for bidding signals or scoring signals, but not both.
// CompressionGroupData are indexed by UnguessableTokens which can be used to
// retrieve them over the auction_worklet::mojom::TrustedSignalsCache Mojo
// interface.
//
// CompressionGroupData objects are created when RequestTrusted*Signals() is
// called and can't reuse an existing one, at which point a new or existing
// Fetch in `fetch_map_` is also associated with the CompressionGroupData.
// Each CompressionGroupData owns all CacheEntries that refer to it, and the
// compression group of the associated fetch as well. No two
// CompressionGroupData objects represent the same compression group from a
// single Fetch.
//
// CompressionGroupData objects are refcounted, and when the last reference is
// released, all associated CacheEntries are destroyed, and the compression
// group of the associated fetch (if the fetche associated with the
// CompressionGroupData has not yet completed) is destroyed as well.
class CompressionGroupData;
// A key that distinguishes bidding signals entries in the cache. The key is
// used to find all potential matching entries whenever
// RequestTrustedBiddingSignals() is invoked. A response with one key cannot
// be used to satisfy a request with another. There are some cases where even
// when the BiddingCacheKey of a new request matches an existing
// BiddingCacheEntry, the entry cannot be reused, in which case a new Entry is
// used and the old one is thrown out (though the CompressionGroupData will
// remain valid). This can happen in the case of cache expiration or the Entry
// not having the necessary `trusted_bidding_signals_keys` or
// `interest_group_name` after the corresponding network request has been sent
// over the wire.
struct BiddingCacheKey {
BiddingCacheKey();
BiddingCacheKey(BiddingCacheKey&&);
// `interest_group_name` should be nullopt in the case of the
// group-by-origin execution mode, in which case all such groups will be
// pooled together, if the other values match, and the interest group names
// will be stored as a value in the BiddingCacheEntry, rather than as part
// of the key.
BiddingCacheKey(const url::Origin& interest_group_owner,
std::optional<std::string> interest_group_name,
const GURL& trusted_signals_url,
const url::Origin& coordinator,
const url::Origin& main_frame_origin,
const url::Origin& joining_origin,
base::Value::Dict additional_params);
~BiddingCacheKey();
BiddingCacheKey& operator=(BiddingCacheKey&&);
bool operator<(const BiddingCacheKey& other) const;
// Values where mismatches are expected to be more likely are listed
// earlier.
// The interest group name, or nullopt, in the case of the group-by-origin
// execution mode, as all such interest groups can be fetched together, in a
// single partition.
std::optional<std::string> interest_group_name;
FetchKey fetch_key;
url::Origin joining_origin;
base::Value::Dict additional_params;
};
// An indexed entry in the cache for callers of
// RequestTrustedBiddingSignals(). It maps InterestGroup information and main
// frame origins to CompressionGroupData objects and partition IDs.
// BiddingCacheEntries that are sent to a TEE together in the same compressed
// partition share a CompressionGroupData, but have different partition ids.
// BiddingCacheEntries are only destroyed when the corresponding
// CompressionGroupData is destroyed, or when a new BiddingCacheEntry with the
// same key replaces them.
struct BiddingCacheEntry;
using BiddingCacheEntryMap = std::map<BiddingCacheKey, BiddingCacheEntry>;
// A key that distinguishes scoring signals entries in the cache. The key is
// used to find all potential matching entries whenever
// RequestTrustedScoringSignals() is invoked. A response with one key cannot
// be used to satisfy a request with another.
//
// Currently, all parameters of each bid appear in ScoringCacheKeys, unlike
// BiddingCacheKeys, so there's no need to check if a ScoringCacheEntry has
// other fields necessary for there to be a cache hit, other than checking the
// TTL. It may be worth experimenting with matching partitioning with that of
// BiddingCacheEntries, but it seems less likely to be useful here, since most
// requests likely have a single renderURL and no componentRenderURLs. And in
// cases where componentRenderURLs are present, it still seems unlikely that
// merging requests will make it more likely all the values for a single bid
// appear in a single ScoringCacheEntry.
struct ScoringCacheKey {
ScoringCacheKey();
ScoringCacheKey(ScoringCacheKey&&);
ScoringCacheKey(const url::Origin& seller,
const GURL& trusted_signals_url,
const url::Origin& coordinator,
const url::Origin& main_frame_origin,
const url::Origin& interest_group_owner,
const url::Origin& joining_origin,
const GURL& render_url,
const std::vector<GURL>& component_render_urls,
base::Value::Dict additional_params);
~ScoringCacheKey();
ScoringCacheKey& operator=(ScoringCacheKey&&);
bool operator<(const ScoringCacheKey& other) const;
// Values where mismatches are expected to be more likely are listed
// earlier.
GURL render_url;
std::set<GURL> component_render_urls;
FetchKey fetch_key;
url::Origin joining_origin;
url::Origin interest_group_owner;
base::Value::Dict additional_params;
};
// An indexed entry in the cache for callers of
// RequestTrustedScoringSignals(). It information about the bid being scored
// and main frame origins to CompressionGroupData objects and partition IDs.
// ScoringCacheEntries that are sent to a TEE together in the same compressed
// partition share a CompressionGroupData, but have different partition ids.
// ScoringCacheEntries are only destroyed when the corresponding
// CompressionGroupData is destroyed, or when they expire (in the latter case,
// the CompressionGroupData may still have outstanding consumers that have yet
// to fetch it, so may still be needed).
struct ScoringCacheEntry;
using ScoringCacheEntryMap = std::map<ScoringCacheKey, ScoringCacheEntry>;
// Returns a CompressionGroupData that can be used to fetch and store data
// associated with the provided FetchKey and joining origin. The returned
// CompressionGroupData will be associated with a Fetch that has not yet
// started, either a new one or a shared one. May return a new or existing
// CompressionGroupData. Queues any newly created fetch. After calling, the
// caller must associate the returned CompressionGroupData with its
// CacheEntry.
//
// `interest_group_owner_if_scoring_signals` is only needed for scoring
// signals fetches. For bidding signals, the `script_owner` of `fetch_key` is
// the `interest_group_owner`, but for scoring signals, it's not, and
// compression groups need to be split by interest group owner, to protect
// against cross-origin size leaks due to compression.
scoped_refptr<TrustedSignalsCacheImpl::CompressionGroupData>
FindOrCreateCompressionGroupDataAndQueueFetch(
const FetchKey& fetch_key,
const url::Origin& joining_origin,
base::optional_ref<const url::Origin>
interest_group_owner_if_scoring_signals);
// Starts retrieving the coordinator key for the specified fetch. Will invoke
// StartFetch() on complete, which may happen synchronously.
void GetCoordinatorKey(FetchMap::iterator fetch_it);
// If the key was successfully fetched, starts the corresponding Fetch.
void OnCoordinatorKeyReceived(
FetchMap::iterator fetch_it,
base::expected<BiddingAndAuctionServerKey, std::string>
bidding_and_auction_server_key);
// Called by StartFetch() to request bidding/scoring signals.
void StartBiddingSignalsFetch(
FetchMap::iterator fetch_it,
const BiddingAndAuctionServerKey& bidding_and_auction_key);
void StartScoringSignalsFetch(
FetchMap::iterator fetch_it,
const BiddingAndAuctionServerKey& bidding_and_auction_key);
void OnFetchComplete(
FetchMap::iterator fetch_it,
TrustedSignalsFetcher::SignalsFetchResult signals_fetch_result);
// Called when the last reference of a CompressionGroupData object has been
// released, and it's about to be destroyed. Does the following:
//
// * Removes the CompressionGroupData from `compression_group_data_`.
//
// * Destroys all CacheEntries associated with it.
//
// * If there is a pending Fetch associated with the CompressionGroupData,
// removes the associated compression block from the Fetch (since the
// CompressionGroupData corresponds to an entire block), cancelling the
// Fetch if it has no non-empty cache blocks. Since compression block IDs
// are not exposed by the API (only partition IDs within the block are),
// there's no need to maintain compression block IDs.
//
// * If there is a live Fetch associated request, the associated compression
// block isn't cleared, but its pointer to the CompressionGroupData is, and
// the Fetch is cancelled if it has no remaining compression blocks
// associated with CompressionGroupData objects.
void OnCompressionGroupDataDestroyed(
CompressionGroupData& compression_group_data);
// Destroys `cache_entry_it` and removes it from the CompressionGroupData that
// owns it. This does not remove data from the compression group. Its
// CompressionGroupData must not have a pending fetch, as that would mean the
// compression group may not retrieve data that a consumer expects it to
// retrieve, since Fetches rely on cache entries to know what to retrieve when
// they're started.
void DestroyBiddingCacheEntry(BiddingCacheEntryMap::iterator cache_entry_it);
void DestroyScoringCacheEntry(ScoringCacheEntryMap::iterator cache_entry_it);
// Virtual for testing.
virtual std::unique_ptr<TrustedSignalsFetcher> CreateFetcher();
const scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
const GetCoordinatorKeyCallback get_coordinator_key_callback_;
mojo::ReceiverSet<auction_worklet::mojom::TrustedSignalsCache,
ReceiverRestrictions>
receiver_set_;
// Multimap of live and pending fetches. Fetches are removed on completion and
// cancellation. When data is requested from the cache, if data needs to be
// fetched from the network and there's an unstarted pending Fetch with a
// matching FetchKey, the pending Fetch will always be used to request the
// additional data. As a result, for any FetchKey, there will be at most one
// pending Fetch, which will be the last Fetch with that FetchKey, since
// multimap entries are stored in FIFO order.
FetchMap fetches_;
BiddingCacheEntryMap bidding_cache_entries_;
ScoringCacheEntryMap scoring_cache_entries_;
// Map of IDs to CompressionGroupData. CompressionGroupData objects are
// refcounted, and removed from the map whenever the last reference is
// released, at which point any associated BiddingCacheEntries are destroyed,
// and the CompressionGroupData removed from any associated Fetch, destroying
// the Fetch if no longer needed.
std::map<base::UnguessableToken,
raw_ptr<CompressionGroupData, CtnExperimental>>
compression_group_data_map_;
};
} // namespace content
#endif // CONTENT_BROWSER_INTEREST_GROUP_TRUSTED_SIGNALS_CACHE_IMPL_H_