blob: 34736d6e74e7b31979508d92baf1f189e63a2ea5 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/interest_group/interest_group_auction.h"
#include <stdint.h>
#include <algorithm>
#include <cmath>
#include <iterator>
#include <list>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/check.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_string_value_serializer.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/safe_conversions.h"
#include "base/rand_util.h"
#include "base/ranges/algorithm.h"
#include "base/strings/escape.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/trace_id_helper.h"
#include "base/types/optional_ref.h"
#include "base/uuid.h"
#include "content/browser/interest_group/ad_auction_page_data.h"
#include "content/browser/interest_group/additional_bid_result.h"
#include "content/browser/interest_group/additional_bids_util.h"
#include "content/browser/interest_group/auction_metrics_recorder.h"
#include "content/browser/interest_group/auction_nonce_manager.h"
#include "content/browser/interest_group/auction_process_manager.h"
#include "content/browser/interest_group/auction_result.h"
#include "content/browser/interest_group/auction_url_loader_factory_proxy.h"
#include "content/browser/interest_group/auction_worklet_manager.h"
#include "content/browser/interest_group/debuggable_auction_worklet.h"
#include "content/browser/interest_group/header_direct_from_seller_signals.h"
#include "content/browser/interest_group/interest_group_auction_reporter.h"
#include "content/browser/interest_group/interest_group_caching_storage.h"
#include "content/browser/interest_group/interest_group_k_anonymity_manager.h"
#include "content/browser/interest_group/interest_group_manager_impl.h"
#include "content/browser/interest_group/interest_group_pa_report_util.h"
#include "content/browser/interest_group/interest_group_priority_util.h"
#include "content/browser/interest_group/storage_interest_group.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/content_browser_client.h"
#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom-forward.h"
#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h"
#include "content/services/auction_worklet/public/mojom/private_aggregation_request.mojom.h"
#include "content/services/auction_worklet/public/mojom/seller_worklet.mojom.h"
#include "crypto/sha2.h"
#include "mojo/public/cpp/bindings/associated_receiver.h"
#include "mojo/public/cpp/bindings/associated_receiver_set.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "net/third_party/quiche/src/quiche/oblivious_http/buffers/oblivious_http_response.h"
#include "services/data_decoder/public/cpp/data_decoder.h"
#include "services/network/public/mojom/client_security_state.mojom.h"
#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/interest_group/ad_auction_constants.h"
#include "third_party/blink/public/common/interest_group/ad_auction_currencies.h"
#include "third_party/blink/public/common/interest_group/ad_display_size_utils.h"
#include "third_party/blink/public/common/interest_group/auction_config.h"
#include "third_party/blink/public/common/interest_group/interest_group.h"
#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
// The BiddingAndAuctionEncryptionMediaType feature controls the format we use
// for the request to the Bidding and Auction Service. anything. When enabled
// we add an extra byte to the request.
CONTENT_EXPORT BASE_FEATURE(kBiddingAndAuctionEncryptionMediaType,
"BiddingAndAuctionEncryptionMediaType",
base::FEATURE_DISABLED_BY_DEFAULT);
// While we would prefer to reference the constants from quiche for the defaults
// here, they are only available as absl::string_view objects which are not
// easily convertible to null-terminated strings. Previously we used the .data()
// accessor which worked due to undefined behavior.
const base::FeatureParam<std::string>
kBiddingAndAuctionEncryptionRequestMediaType{
&kBiddingAndAuctionEncryptionMediaType,
"BiddingAndAuctionEncryptionRequestMediaType", "message/bhttp request"};
const base::FeatureParam<std::string>
kBiddingAndAuctionEncryptionResponseMediaType{
&kBiddingAndAuctionEncryptionMediaType,
"BiddingAndAuctionEncryptionResponseMediaType",
"message/bhttp response"};
namespace {
constexpr base::TimeDelta kMaxPerBuyerTimeout = base::Milliseconds(500);
// For group freshness metrics.
constexpr base::TimeDelta kGroupFreshnessMin = base::Minutes(1);
constexpr base::TimeDelta kGroupFreshnessMax = base::Days(30);
constexpr int kGroupFreshnessBuckets = 100;
// All URLs received from worklets must be valid HTTPS URLs. It's up to callers
// to call ReportBadMessage() on invalid URLs.
bool IsUrlValid(const GURL& url) {
return url.is_valid() && url.SchemeIs(url::kHttpsScheme);
}
bool IsKAnon(const base::flat_map<std::string, bool>& kanon_keys,
const std::string& key) {
auto it = kanon_keys.find(key);
return it != kanon_keys.end() && it->second;
}
bool IsKAnon(const base::flat_map<std::string, bool>& kanon_keys,
const blink::InterestGroup& interest_group,
const auction_worklet::mojom::BidderWorkletBid* bid) {
if (!IsKAnon(kanon_keys,
blink::KAnonKeyForAdBid(interest_group, bid->ad_descriptor))) {
return false;
}
if (bid->ad_component_descriptors.has_value()) {
for (const blink::AdDescriptor& component :
bid->ad_component_descriptors.value()) {
if (!IsKAnon(kanon_keys, blink::KAnonKeyForAdComponentBid(component))) {
return false;
}
}
}
return true;
}
base::flat_map<auction_worklet::mojom::KAnonKeyPtr, bool> KAnonKeysToMojom(
const base::flat_map<std::string, bool>& kanon_keys) {
std::vector<std::pair<auction_worklet::mojom::KAnonKeyPtr, bool>> result;
for (const auto& key : kanon_keys) {
result.emplace_back(auction_worklet::mojom::KAnonKey::New(key.first),
key.second);
}
return std::move(result);
}
// Finds InterestGroup::Ad in `ads` that matches `ad_descriptor`, if any.
// Returns nullptr if `ad_descriptor` is invalid.
const blink::InterestGroup::Ad* FindMatchingAd(
const std::vector<blink::InterestGroup::Ad>& ads,
const base::flat_map<std::string, bool>& kanon_keys,
const blink::InterestGroup& interest_group,
InterestGroupAuction::Bid::BidRole bid_role,
bool is_component_ad,
const blink::AdDescriptor& ad_descriptor) {
// TODO(mmenke): Validate render URLs on load and make this a DCHECK just
// before the return instead, since then `ads` will necessarily only contain
// valid URLs at that point.
if (!IsUrlValid(ad_descriptor.url)) {
return nullptr;
}
if (ad_descriptor.size && !IsValidAdSize(ad_descriptor.size.value())) {
return nullptr;
}
if (bid_role != InterestGroupAuction::Bid::BidRole::kUnenforcedKAnon) {
const std::string kanon_key =
is_component_ad
? blink::KAnonKeyForAdComponentBid(ad_descriptor)
: blink::KAnonKeyForAdBid(interest_group, ad_descriptor);
if (!IsKAnon(kanon_keys, kanon_key)) {
return nullptr;
}
}
for (const auto& ad : ads) {
if (ad.render_url != ad_descriptor.url) {
continue;
}
if (!ad.size_group && !ad_descriptor.size) {
// Neither `blink::InterestGroup::Ad` nor the ad from the bid have any
// size specifications. They are considered as matching ad as long as
// they have the same url.
return &ad;
}
if (!ad.size_group != !ad_descriptor.size) {
// When only one of the two ads has a size specification, they are
// considered matching. The caller is responsible for discarding the
// extraneous size information
return &ad;
}
// Both `blink::InterestGroup::Ad` and the ad from the bid have size
// specifications. They are considered as matching ad only if their
// size also matches.
auto has_matching_ad_size = [&interest_group,
&ad_descriptor](const std::string& ad_size) {
return interest_group.ad_sizes->at(ad_size) == *ad_descriptor.size;
};
if (base::ranges::any_of(interest_group.size_groups->at(ad.size_group),
has_matching_ad_size)) {
// Each size group may also correspond to multiple ad sizes. If any of
// those ad sizes matches with the ad size from `ad_descriptor`, they are
// considered as matching ads.
return &ad;
}
}
return nullptr;
}
// Checks that `bid` is a valid bid value for an auction.
bool IsValidBid(double bid) {
return !std::isnan(bid) && std::isfinite(bid) && bid > 0;
}
struct BidStatesDescByPriority {
bool operator()(const std::unique_ptr<InterestGroupAuction::BidState>& a,
const std::unique_ptr<InterestGroupAuction::BidState>& b) {
return a->calculated_priority > b->calculated_priority;
}
bool operator()(const std::unique_ptr<InterestGroupAuction::BidState>& a,
double b_priority) {
return a->calculated_priority > b_priority;
}
bool operator()(double a_priority,
const std::unique_ptr<InterestGroupAuction::BidState>& b) {
return a_priority > b->calculated_priority;
}
};
struct BidStatesDescByPriorityAndGroupByJoinOrigin {
bool operator()(const std::unique_ptr<InterestGroupAuction::BidState>& a,
const std::unique_ptr<InterestGroupAuction::BidState>& b) {
return std::tie(a->calculated_priority, a->bidder->joining_origin,
a->bidder->interest_group.execution_mode) >
std::tie(b->calculated_priority, b->bidder->joining_origin,
b->bidder->interest_group.execution_mode);
}
};
bool IsBidRoleUsedForWinner(
auction_worklet::mojom::KAnonymityBidMode kanon_mode,
InterestGroupAuction::Bid::BidRole bid_role) {
if (kanon_mode == auction_worklet::mojom::KAnonymityBidMode::kEnforce) {
return bid_role != InterestGroupAuction::Bid::BidRole::kUnenforcedKAnon;
} else {
return bid_role != InterestGroupAuction::Bid::BidRole::kEnforcedKAnon;
}
}
static const char* ScoreAdTraceEventName(const InterestGroupAuction::Bid& bid) {
if (bid.bid_role == InterestGroupAuction::Bid::BidRole::kEnforcedKAnon) {
return "seller_worklet_score_kanon_enforced_ad";
} else {
return "seller_worklet_score_ad";
}
}
// Returns true iff `interest_group` grants `seller` all the capabilities in
// `capabilities`.
bool GroupSatisfiesAllCapabilities(const blink::InterestGroup& interest_group,
blink::SellerCapabilitiesType capabilities,
const url::Origin& seller) {
if (interest_group.seller_capabilities) {
auto it = interest_group.seller_capabilities->find(seller);
if (it != interest_group.seller_capabilities->end()) {
return it->second.HasAll(capabilities);
}
}
return interest_group.all_sellers_capabilities.HasAll(capabilities);
}
// Helper for ReportPaBuyersValueIfAllowed() -- returns true iff
// `interest_group`'s seller capabilities has authorized `capability` for
// `seller`.
bool CanReportPaBuyersValue(const blink::InterestGroup& interest_group,
blink::SellerCapabilities capability,
const url::Origin& seller) {
return GroupSatisfiesAllCapabilities(interest_group, {capability}, seller);
}
// Helper for ReportPaBuyersValueIfAllowed() -- returns the bucket base
// of `buyer`, if present in `config`'s `auction_report_buyer_keys`.
absl::optional<absl::uint128> BucketBaseForReportPaBuyers(
const blink::AuctionConfig& config,
const url::Origin& buyer) {
if (!config.non_shared_params.auction_report_buyer_keys) {
return absl::nullopt;
}
// Find the index of the buyer in `buyers`. It should be present, since we
// only load interest groups belonging to owners from `buyers`.
DCHECK(config.non_shared_params.interest_group_buyers);
const std::vector<url::Origin>& buyers =
*config.non_shared_params.interest_group_buyers;
absl::optional<size_t> index;
for (size_t i = 0; i < buyers.size(); i++) {
if (buyer == buyers.at(i)) {
index = i;
break;
}
}
DCHECK(index);
// Use that index to get the associated bucket base, if present.
if (*index >= config.non_shared_params.auction_report_buyer_keys->size()) {
return absl::nullopt;
}
return config.non_shared_params.auction_report_buyer_keys->at(*index);
}
// Helper for ReportPaBuyersValueIfAllowed() -- returns the
// AuctionReportBuyersConfig for `buyer_report_type`, if it exists in
// `auction_report_buyers` in `config`.
absl::optional<blink::AuctionConfig::NonSharedParams::AuctionReportBuyersConfig>
ReportBuyersConfigForPaBuyers(
blink::AuctionConfig::NonSharedParams::BuyerReportType buyer_report_type,
const blink::AuctionConfig& config) {
if (!config.non_shared_params.auction_report_buyers) {
return absl::nullopt;
}
const auto& report_buyers = *config.non_shared_params.auction_report_buyers;
auto it = report_buyers.find(buyer_report_type);
if (it == report_buyers.end()) {
return absl::nullopt;
}
return it->second;
}
// Takes private aggregation requests for `state`, if there are any, and moves
// them into `private_aggregation_requests_reserved` and
// `private_aggregation_requests_non_reserved`.
//
// Calculates bucket/value using `signals` and `top_level_signals` as needed.
//
// `winner` points to the BidState associated with the winning bid, if there
// is one.
//
// `signals` are the PostAuctionSignals from the auction `state` was a part of.
void TakePrivateAggregationRequestsForBidState(
std::unique_ptr<InterestGroupAuction::BidState>& state,
bool is_component_auction,
const InterestGroupAuction::BidState* winner,
const InterestGroupAuction::BidState* non_kanon_winner,
const InterestGroupAuction::PostAuctionSignals& signals,
const absl::optional<InterestGroupAuction::PostAuctionSignals>&
top_level_signals,
std::map<InterestGroupAuctionReporter::PrivateAggregationKey,
InterestGroupAuctionReporter::PrivateAggregationRequests>&
private_aggregation_requests_reserved,
std::map<std::string,
InterestGroupAuctionReporter::PrivateAggregationRequests>&
private_aggregation_requests_non_reserved) {
bool is_winner = state.get() == winner;
for (auto& [key, requests] : state->private_aggregation_requests) {
const url::Origin& origin = key.reporting_origin;
InterestGroupAuction::PrivateAggregationPhase phase = key.phase;
const absl::optional<url::Origin>& aggregation_coordinator_origin =
key.aggregation_coordinator_origin;
double winning_bid_to_use = signals.winning_bid;
double highest_scoring_other_bid_to_use = signals.highest_scoring_other_bid;
// When component auctions are in use, a BuyerHelper for a component
// auction calls here for the scoreAd() aggregation calls from the
// top-level; in that case the relevant signals are in
// `top_level_signals` and not `signals`. `highest_scoring_other_bid`
// is also not reported for top-levels.
if (phase ==
InterestGroupAuction::PrivateAggregationPhase::kTopLevelSeller &&
is_component_auction) {
highest_scoring_other_bid_to_use = 0;
winning_bid_to_use =
top_level_signals.has_value() ? top_level_signals->winning_bid : 0.0;
}
for (auction_worklet::mojom::PrivateAggregationRequestPtr& request :
requests) {
absl::optional<PrivateAggregationRequestWithEventType> converted_request =
FillInPrivateAggregationRequest(
std::move(request), winning_bid_to_use,
highest_scoring_other_bid_to_use, state->reject_reason,
state->pa_timings(phase), is_winner);
if (converted_request.has_value()) {
PrivateAggregationRequestWithEventType converted_request_value =
std::move(converted_request.value());
const absl::optional<std::string>& event_type =
converted_request_value.event_type;
if (event_type.has_value()) {
// The request has a non-reserved event type.
private_aggregation_requests_non_reserved[event_type.value()]
.emplace_back(std::move(converted_request_value.request));
} else {
InterestGroupAuctionReporter::PrivateAggregationKey agg_key = {
origin, aggregation_coordinator_origin};
private_aggregation_requests_reserved[std::move(agg_key)]
.emplace_back(std::move(converted_request_value.request));
}
}
}
}
if (non_kanon_winner == state.get()) {
const url::Origin& bidder = state->bidder->interest_group.owner;
const absl::optional<url::Origin>& aggregation_coordinator_origin =
state->bidder->interest_group.aggregation_coordinator_origin;
for (auction_worklet::mojom::PrivateAggregationRequestPtr& request :
state->non_kanon_private_aggregation_requests) {
absl::optional<PrivateAggregationRequestWithEventType> converted_request =
FillInPrivateAggregationRequest(
std::move(request), signals.winning_bid,
signals.highest_scoring_other_bid,
auction_worklet::mojom::RejectReason::kBelowKAnonThreshold,
state->pa_timings(
InterestGroupAuction::PrivateAggregationPhase::kBidder),
false);
if (converted_request.has_value()) {
InterestGroupAuctionReporter::PrivateAggregationKey agg_key = {
bidder, aggregation_coordinator_origin};
PrivateAggregationRequestWithEventType converted_request_value =
std::move(converted_request.value());
// Only reserved types are supported for k-anon failures.
// This *should* be guaranteed by `FillInPrivateAggregationRequest`
// since we passed in `false` for `is_winner`.
DCHECK(!converted_request_value.event_type.has_value());
private_aggregation_requests_reserved[std::move(agg_key)].emplace_back(
std::move(converted_request_value.request));
}
}
}
}
// Adds debug reporting URLs for `bid_state` to `debug_win_report_urls` and
// `debug_loss_report_urls`, if there are any, filling in report URL template
// parameters as needed. The URLs are moved away from `bid_state`.
//
// `winner` points to the BidState associated with the winning bid, if there
// is one.
//
// `signals` are the PostAuctionSignals from the auction `state` was a part of.
//
// `top_level_signals` are the PostAuctionSignals of the top-level auction, if
// the computation is for a component auction, and nullopt otherwise.
void TakeDebugReportUrlsForBidState(
std::unique_ptr<InterestGroupAuction::BidState>& bid_state,
const InterestGroupAuction::BidState* winner,
const InterestGroupAuction::PostAuctionSignals& signals,
const absl::optional<InterestGroupAuction::PostAuctionSignals>&
top_level_signals,
std::vector<GURL>& debug_win_report_urls,
std::vector<GURL>& debug_loss_report_urls) {
if (bid_state.get() == winner) {
if (winner->bidder_debug_win_report_url.has_value()) {
debug_win_report_urls.emplace_back(
InterestGroupAuction::FillPostAuctionSignals(
std::move(winner->bidder_debug_win_report_url).value(), signals));
}
if (winner->seller_debug_win_report_url.has_value()) {
debug_win_report_urls.emplace_back(
InterestGroupAuction::FillPostAuctionSignals(
std::move(winner->seller_debug_win_report_url).value(), signals,
top_level_signals));
}
// `top_level_signals` is passed as parameter `signals` for top-level
// seller.
if (winner->top_level_seller_debug_win_report_url.has_value()) {
debug_win_report_urls.emplace_back(
InterestGroupAuction::FillPostAuctionSignals(
std::move(winner->top_level_seller_debug_win_report_url).value(),
top_level_signals.value()));
}
return;
}
if (bid_state->bidder_debug_loss_report_url.has_value()) {
// Losing and rejected bidders should not get highest_scoring_other_bid
// and made_highest_scoring_other_bid signals. (And also the currency
// bit for those).
debug_loss_report_urls.emplace_back(
InterestGroupAuction::FillPostAuctionSignals(
std::move(bid_state->bidder_debug_loss_report_url).value(),
InterestGroupAuction::PostAuctionSignals(
signals.winning_bid, signals.winning_bid_currency,
signals.made_winning_bid, /*highest_scoring_other_bid=*/0.0,
/*highest_scoring_other_bid_currency=*/absl::nullopt,
/*made_highest_scoring_other_bid=*/false),
/*top_level_signals=*/absl::nullopt, bid_state->reject_reason));
}
// TODO(qingxinwu): Add reject reason to seller debug loss report as well.
if (bid_state->seller_debug_loss_report_url.has_value()) {
debug_loss_report_urls.emplace_back(
InterestGroupAuction::FillPostAuctionSignals(
std::move(bid_state->seller_debug_loss_report_url).value(), signals,
top_level_signals));
}
// `top_level_signals` is passed as parameter `signals` for top-level
// seller.
if (bid_state->top_level_seller_debug_loss_report_url.has_value()) {
debug_loss_report_urls.emplace_back(
InterestGroupAuction::FillPostAuctionSignals(
std::move(bid_state->top_level_seller_debug_loss_report_url)
.value(),
top_level_signals.value()));
}
}
// Retrieves the timeout from `buyer_timeouts` associated with `buyer`, if any.
// Used for both `buyer_timeouts` and `buyer_cumulative_timeouts`, stored in
// AuctionConfigs. Callers should use PerBuyerTimeout() and
// PerBuyerCumulativeTimeout() instead, since those apply the timeout limit,
// when applicable.
absl::optional<base::TimeDelta> PerBuyerTimeoutHelper(
const url::Origin& buyer,
const blink::AuctionConfig::MaybePromiseBuyerTimeouts& buyer_timeouts) {
DCHECK(!buyer_timeouts.is_promise());
const auto& per_buyer_timeouts = buyer_timeouts.value().per_buyer_timeouts;
if (per_buyer_timeouts.has_value()) {
auto it = per_buyer_timeouts->find(buyer);
if (it != per_buyer_timeouts->end()) {
return it->second;
}
}
const auto& all_buyers_timeout = buyer_timeouts.value().all_buyers_timeout;
if (all_buyers_timeout.has_value()) {
return all_buyers_timeout.value();
}
return absl::nullopt;
}
absl::optional<base::TimeDelta> PerBuyerTimeout(
const url::Origin& buyer,
const blink::AuctionConfig& auction_config) {
absl::optional<base::TimeDelta> out = PerBuyerTimeoutHelper(
buyer, auction_config.non_shared_params.buyer_timeouts);
if (!out) {
return out;
}
return std::min(*out, kMaxPerBuyerTimeout);
}
absl::optional<base::TimeDelta> PerBuyerCumulativeTimeout(
const url::Origin& buyer,
const blink::AuctionConfig& auction_config) {
return PerBuyerTimeoutHelper(
buyer, auction_config.non_shared_params.buyer_cumulative_timeouts);
}
absl::optional<blink::AdCurrency> PerBuyerCurrency(
const url::Origin& buyer,
const blink::AuctionConfig& auction_config) {
const blink::AuctionConfig::MaybePromiseBuyerCurrencies& buyer_currencies =
auction_config.non_shared_params.buyer_currencies;
DCHECK(!buyer_currencies.is_promise());
const auto& per_buyer_currencies =
buyer_currencies.value().per_buyer_currencies;
if (per_buyer_currencies.has_value()) {
auto it = per_buyer_currencies->find(buyer);
if (it != per_buyer_currencies->end()) {
return it->second;
}
}
const auto& all_buyers_currency =
buyer_currencies.value().all_buyers_currency;
return all_buyers_currency; // Maybe nullopt.
}
} // namespace
InterestGroupAuction::PostAuctionSignals::PostAuctionSignals() = default;
InterestGroupAuction::PostAuctionSignals::PostAuctionSignals(
double winning_bid,
absl::optional<blink::AdCurrency> winning_bid_currency,
bool made_winning_bid)
: winning_bid(winning_bid),
winning_bid_currency(std::move(winning_bid_currency)),
made_winning_bid(made_winning_bid) {}
InterestGroupAuction::PostAuctionSignals::PostAuctionSignals(
double winning_bid,
absl::optional<blink::AdCurrency> winning_bid_currency,
bool made_winning_bid,
double highest_scoring_other_bid,
absl::optional<blink::AdCurrency> highest_scoring_other_bid_currency,
bool made_highest_scoring_other_bid)
: winning_bid(winning_bid),
winning_bid_currency(std::move(winning_bid_currency)),
made_winning_bid(made_winning_bid),
highest_scoring_other_bid(highest_scoring_other_bid),
highest_scoring_other_bid_currency(
std::move(highest_scoring_other_bid_currency)),
made_highest_scoring_other_bid(made_highest_scoring_other_bid) {}
InterestGroupAuction::PostAuctionSignals::~PostAuctionSignals() = default;
// static
void InterestGroupAuction::PostAuctionSignals::FillWinningBidInfo(
const url::Origin& owner,
absl::optional<url::Origin> winner_owner,
double winning_bid,
absl::optional<double> winning_bid_in_seller_currency,
const absl::optional<blink::AdCurrency>& seller_currency,
bool& out_made_winning_bid,
double& out_winning_bid,
absl::optional<blink::AdCurrency>& out_winning_bid_currency) {
out_made_winning_bid = false;
if (winner_owner.has_value()) {
out_made_winning_bid = owner == *winner_owner;
}
if (seller_currency.has_value()) {
out_winning_bid = winning_bid_in_seller_currency.value_or(0.0);
out_winning_bid_currency = *seller_currency;
} else {
out_winning_bid = winning_bid;
out_winning_bid_currency = absl::nullopt;
}
}
// static
void InterestGroupAuction::PostAuctionSignals::
FillRelevantHighestScoringOtherBidInfo(
const url::Origin& owner,
absl::optional<url::Origin> highest_scoring_other_bid_owner,
double highest_scoring_other_bid,
absl::optional<double> highest_scoring_other_bid_in_seller_currency,
const absl::optional<blink::AdCurrency>& seller_currency,
bool& out_made_highest_scoring_other_bid,
double& out_highest_scoring_other_bid,
absl::optional<blink::AdCurrency>&
out_highest_scoring_other_bid_currency) {
out_made_highest_scoring_other_bid = false;
if (highest_scoring_other_bid_owner.has_value()) {
DCHECK_GT(highest_scoring_other_bid, 0);
out_made_highest_scoring_other_bid =
owner == highest_scoring_other_bid_owner.value();
}
if (seller_currency.has_value()) {
out_highest_scoring_other_bid =
highest_scoring_other_bid_in_seller_currency.value_or(0);
out_highest_scoring_other_bid_currency = *seller_currency;
} else {
out_highest_scoring_other_bid = highest_scoring_other_bid;
out_highest_scoring_other_bid_currency = absl::nullopt;
}
}
InterestGroupAuction::BidState::PrivateAggregationPhaseKey::
PrivateAggregationPhaseKey(
url::Origin reporting_origin,
PrivateAggregationPhase phase,
absl::optional<url::Origin> aggregation_coordinator_origin)
: reporting_origin(reporting_origin),
phase(phase),
aggregation_coordinator_origin(aggregation_coordinator_origin) {}
InterestGroupAuction::BidState::PrivateAggregationPhaseKey::
PrivateAggregationPhaseKey(const PrivateAggregationPhaseKey&) = default;
InterestGroupAuction::BidState::PrivateAggregationPhaseKey&
InterestGroupAuction::BidState::PrivateAggregationPhaseKey::operator=(
const PrivateAggregationPhaseKey&) = default;
InterestGroupAuction::BidState::PrivateAggregationPhaseKey::
PrivateAggregationPhaseKey(PrivateAggregationPhaseKey&&) = default;
InterestGroupAuction::BidState::PrivateAggregationPhaseKey&
InterestGroupAuction::BidState::PrivateAggregationPhaseKey::operator=(
PrivateAggregationPhaseKey&&) = default;
InterestGroupAuction::BidState::PrivateAggregationPhaseKey::
~PrivateAggregationPhaseKey() = default;
InterestGroupAuction::BidState::~BidState() {
if (trace_id.has_value()) {
EndTracing();
}
if (trace_id_for_kanon_scoring.has_value()) {
EndTracingKAnonScoring();
}
}
InterestGroupAuction::BidState::BidState(BidState&&) = default;
InterestGroupAuction::BidState::BidState(
const SingleStorageInterestGroup&& bidder)
: bidder(std::move(bidder)) {}
void InterestGroupAuction::BidState::BeginTracing() {
DCHECK(!trace_id.has_value());
trace_id = base::trace_event::GetNextGlobalTraceId();
const blink::InterestGroup& interest_group = bidder->interest_group;
TRACE_EVENT_NESTABLE_ASYNC_BEGIN2("fledge", "bid", *trace_id, "bidding_url",
interest_group.bidding_url,
"interest_group_name", interest_group.name);
}
void InterestGroupAuction::BidState::EndTracing() {
DCHECK(trace_id.has_value());
TRACE_EVENT_NESTABLE_ASYNC_END0("fledge", "bid", *trace_id);
trace_id = absl::nullopt;
}
void InterestGroupAuction::BidState::BeginTracingKAnonScoring() {
DCHECK(!trace_id_for_kanon_scoring.has_value());
trace_id_for_kanon_scoring = base::trace_event::GetNextGlobalTraceId();
const blink::InterestGroup& interest_group = bidder->interest_group;
TRACE_EVENT_NESTABLE_ASYNC_BEGIN2("fledge", "score_kanon_enforced_ad",
*trace_id_for_kanon_scoring, "bidding_url",
interest_group.bidding_url,
"interest_group_name", interest_group.name);
}
void InterestGroupAuction::BidState::EndTracingKAnonScoring() {
DCHECK(trace_id_for_kanon_scoring.has_value());
TRACE_EVENT_NESTABLE_ASYNC_END0("fledge", "score_kanon_enforced_ad",
*trace_id_for_kanon_scoring);
trace_id_for_kanon_scoring = absl::nullopt;
}
InterestGroupAuction::Bid::Bid(
BidRole bid_role,
std::string ad_metadata,
double bid,
absl::optional<blink::AdCurrency> bid_currency,
absl::optional<double> ad_cost,
blink::AdDescriptor ad_descriptor,
std::vector<blink::AdDescriptor> ad_component_descriptors,
absl::optional<uint16_t> modeling_signals,
base::TimeDelta bid_duration,
absl::optional<uint32_t> bidding_signals_data_version,
const blink::InterestGroup::Ad* bid_ad,
BidState* bid_state,
InterestGroupAuction* auction)
: bid_role(bid_role),
ad_metadata(std::move(ad_metadata)),
bid(bid),
bid_currency(std::move(bid_currency)),
ad_cost(std::move(ad_cost)),
ad_descriptor(std::move(ad_descriptor)),
ad_component_descriptors(std::move(ad_component_descriptors)),
modeling_signals(modeling_signals),
bid_duration(bid_duration),
bidding_signals_data_version(bidding_signals_data_version),
interest_group(&bid_state->bidder->interest_group),
bid_ad(bid_ad),
bid_state(bid_state),
auction(auction) {
DCHECK(IsValidBid(bid));
}
InterestGroupAuction::Bid::Bid(Bid&) = default;
InterestGroupAuction::Bid::~Bid() = default;
std::vector<GURL> InterestGroupAuction::Bid::GetAdComponentUrls() const {
std::vector<GURL> ad_component_urls;
ad_component_urls.reserve(ad_component_descriptors.size());
base::ranges::transform(
ad_component_descriptors, std::back_inserter(ad_component_urls),
[](const blink::AdDescriptor& ad_component_descriptor) {
return ad_component_descriptor.url;
});
return ad_component_urls;
}
InterestGroupAuction::ScoredBid::ScoredBid(
double score,
absl::optional<uint32_t> scoring_signals_data_version,
std::unique_ptr<Bid> bid,
absl::optional<double> bid_in_seller_currency,
auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr
component_auction_modified_bid_params)
: score(score),
scoring_signals_data_version(scoring_signals_data_version),
bid(std::move(bid)),
bid_in_seller_currency(std::move(bid_in_seller_currency)),
component_auction_modified_bid_params(
std::move(component_auction_modified_bid_params)) {
DCHECK_GT(score, 0);
}
InterestGroupAuction::ScoredBid::~ScoredBid() = default;
// Every interest group owner participating in an auctions gets its own
// BuyerHelper. The class is responsible for handing buyer-side calls during
// the bidding/scoring phase.
//
// In particular, it handles:
// * Sorting interest groups that share a bidder by priority.
// * Deciding which interest groups get to bid.
// * Creating BidderWorklets.
// * Calling BidderWorklet::GenerateBid().
// * Tracking how many interest groups the buyer owns that still need to
// bid.
class InterestGroupAuction::BuyerHelper
: public auction_worklet::mojom::GenerateBidClient {
public:
// `auction` is expected to own the BuyerHelper, and therefore outlive it.
BuyerHelper(InterestGroupAuction* auction,
std::vector<SingleStorageInterestGroup>&& interest_groups)
: auction_(auction), owner_(interest_groups[0]->interest_group.owner) {
DCHECK(!interest_groups.empty());
// Move interest groups to `bid_states_` and update priorities using
// `priority_vector`, if present. Delete groups where the calculation
// results in a priority < 0.
for (SingleStorageInterestGroup& bidder : interest_groups) {
double priority = bidder->interest_group.priority;
if (bidder->interest_group.priority_vector &&
!bidder->interest_group.priority_vector->empty()) {
priority = CalculateInterestGroupPriority(
*auction_->config_, *bidder, auction_->auction_start_time_,
*bidder->interest_group.priority_vector);
// Only filter interest groups with priority < 0 if the negative
// priority is the result of a `priority_vector` multiplication.
//
// TODO(mmenke): If we can make this the standard behavior for the
// `priority` field as well, the API would be more consistent.
if (priority < 0) {
auction_->auction_metrics_recorder_
->RecordBidFilteredDuringInterestGroupLoad();
continue;
}
}
if (bidder->interest_group.enable_bidding_signals_prioritization) {
enable_bidding_signals_prioritization_ = true;
}
auto state = std::make_unique<BidState>(std::move(bidder));
state->calculated_priority = priority;
bid_states_.emplace_back(std::move(state));
}
size_limit_ = auction_->config_->non_shared_params.all_buyers_group_limit;
const auto limit_iter =
auction_->config_->non_shared_params.per_buyer_group_limits.find(
owner_);
if (limit_iter !=
auction_->config_->non_shared_params.per_buyer_group_limits.cend()) {
size_limit_ = static_cast<size_t>(limit_iter->second);
}
size_limit_ = std::min(bid_states_.size(), size_limit_);
if (size_limit_ == 0) {
bid_states_.clear();
return;
}
if (!enable_bidding_signals_prioritization_) {
ApplySizeLimitAndSort();
} else {
// When not applying the size limit yet, still sort by priority, since
// worklets preserve the order they see requests in. This allows higher
// priority interest groups will get to bid first, and also groups
// interest groups by the origin they joined to potentially improve
// Javscript context reuse.
SortByPriorityAndGroupByJoinOrigin();
}
// Figure out which BidState is last for each key, as it will be responsible
// for sending out the trusted bidder signals request.
std::set<AuctionWorkletManager::WorkletKey> seen_keys;
for (auto it = bid_states_.rbegin(); it != bid_states_.rend(); ++it) {
std::unique_ptr<BidState>& bid_state = *it;
auto [iter, success] =
seen_keys.insert(auction_->BidderWorkletKey(*bid_state));
bid_state->send_pending_trusted_signals_after_generate_bid = success;
}
}
~BuyerHelper() override = default;
// Requests bidder worklets and starts generating bids. May generate no bids,
// 1 bid, or multiple bids. Invokes owning InterestGroupAuction's
// ScoreBidIfReady() for each bid generated, and OnBidderDone() once all bids
// have been generated. OnBidderDone() is always invoked asynchronously.
void StartGeneratingBids() {
DCHECK(!bid_states_.empty());
DCHECK_EQ(0, num_outstanding_bids_);
num_outstanding_bids_ = bid_states_.size();
num_outstanding_bidding_signals_received_calls_ = num_outstanding_bids_;
start_generating_bids_time_ = base::TimeTicks::Now();
// Request processes for all bidder worklets.
for (auto& bid_state : bid_states_) {
auto worklet_key = auction_->BidderWorkletKey(*bid_state);
auction_->auction_metrics_recorder_->ReportBidderWorkletKey(worklet_key);
auction_->auction_worklet_manager_->RequestWorkletByKey(
worklet_key,
base::BindOnce(&BuyerHelper::OnBidderWorkletReceived,
base::Unretained(this), bid_state.get()),
base::BindOnce(&BuyerHelper::OnBidderWorkletGenerateBidFatalError,
base::Unretained(this), bid_state.get()),
bid_state->worklet_handle);
}
}
// auction_worklet::mojom::GenerateBidClient implementation:
void OnBiddingSignalsReceived(
const base::flat_map<std::string, double>& priority_vector,
base::TimeDelta trusted_signals_fetch_latency,
base::OnceClosure resume_generate_bid_callback) override {
BidState* state = generate_bid_client_receiver_set_.current_context();
const blink::InterestGroup& interest_group = state->bidder->interest_group;
state->pa_timings(PrivateAggregationPhase::kBidder).signals_fetch_time =
trusted_signals_fetch_latency;
auction_->ReportTrustedSignalsFetchLatency(interest_group,
trusted_signals_fetch_latency);
absl::optional<double> new_priority;
if (!priority_vector.empty()) {
new_priority = CalculateInterestGroupPriority(
*auction_->config_, *(state->bidder), auction_->auction_start_time_,
priority_vector,
(interest_group.priority_vector &&
!interest_group.priority_vector->empty())
? state->calculated_priority
: absl::optional<double>());
if (*new_priority < 0) {
auction_->auction_metrics_recorder_
->RecordBidFilteredDuringReprioritization();
}
}
OnBiddingSignalsReceivedInternal(state, new_priority,
std::move(resume_generate_bid_callback));
}
void OnGenerateBidComplete(
auction_worklet::mojom::BidderWorkletBidPtr mojo_bid,
auction_worklet::mojom::BidderWorkletKAnonEnforcedBidPtr mojo_kanon_bid,
uint32_t bidding_signals_data_version,
bool has_bidding_signals_data_version,
const absl::optional<GURL>& debug_loss_report_url,
const absl::optional<GURL>& debug_win_report_url,
double set_priority,
bool has_set_priority,
base::flat_map<std::string,
auction_worklet::mojom::PrioritySignalsDoublePtr>
update_priority_signals_overrides,
PrivateAggregationRequests pa_requests,
PrivateAggregationRequests non_kanon_pa_requests,
base::TimeDelta bidding_latency,
auction_worklet::mojom::GenerateBidDependencyLatenciesPtr
generate_bid_dependency_latencies,
auction_worklet::mojom::RejectReason reject_reason,
const std::vector<std::string>& errors) override {
BidState* state = generate_bid_client_receiver_set_.current_context();
const blink::InterestGroup& interest_group = state->bidder->interest_group;
state->pa_timings(PrivateAggregationPhase::kBidder).script_run_time =
bidding_latency;
auction_->ReportBiddingLatency(interest_group, bidding_latency);
// This is intentionally recorded here as opposed to in
// OnGenerateBidCompleteInternal in order to exclude bids that were
// filtered during reprioritization. It also excludes those bids that
// encountered a fatal error, except for timeouts; those we record to this
// metric separately and explicitly in OnTimeout.
auction_->auction_metrics_recorder_->RecordBidForOneInterestGroupLatency(
base::TimeTicks::Now() - start_generating_bids_time_);
auction_->auction_metrics_recorder_->RecordGenerateBidDependencyLatencies(
*generate_bid_dependency_latencies);
OnGenerateBidCompleteInternal(
state, std::move(mojo_bid), std::move(mojo_kanon_bid),
bidding_signals_data_version, has_bidding_signals_data_version,
debug_loss_report_url, debug_win_report_url, set_priority,
has_set_priority, std::move(update_priority_signals_overrides),
std::move(pa_requests), std::move(non_kanon_pa_requests), reject_reason,
errors);
}
// Closes all Mojo pipes, releases all weak pointers, and stops the timeout
// timer.
void ClosePipes() {
weak_ptr_factory_.InvalidateWeakPtrs();
for (auto& bid_state : bid_states_) {
CloseBidStatePipes(*bid_state);
}
// No need to clear `generate_bid_client_receiver_set_`, since
// CloseBidStatePipes() should take care of that.
DCHECK(generate_bid_client_receiver_set_.empty());
// Need to stop the timer - this is called on completion and on certain
// errors. Don't want the timer to trigger anything after there's been a
// failure already.
cumulative_buyer_timeout_timer_.Stop();
}
// Returns true if this buyer has any interest groups that will potentially
// bid in an auction -- that is, not all interest groups have been filtered
// out.
bool has_potential_bidder() const { return !bid_states_.empty(); }
size_t num_potential_bidders() const { return bid_states_.size(); }
const url::Origin& owner() const { return owner_; }
void GetInterestGroupsThatBidAndReportBidCounts(
blink::InterestGroupSet& interest_groups) const {
size_t bid_count = 0;
for (const auto& bid_state : bid_states_) {
if (bid_state->made_bid) {
interest_groups.emplace(bid_state->bidder->interest_group.owner,
bid_state->bidder->interest_group.name);
auction_->interest_group_manager_->NotifyInterestGroupAccessed(
InterestGroupManagerImpl::InterestGroupObserver::kBid,
bid_state->bidder->interest_group.owner,
bid_state->bidder->interest_group.name);
bid_count++;
}
}
for (const auto& bid_state : bid_states_) {
if (auction_->ReportBidCount(bid_state->bidder->interest_group,
bid_count)) {
break;
}
}
}
// Adds debug reporting URLs to `debug_win_report_urls` and
// `debug_loss_report_urls`, if there are any, filling in report URL template
// parameters as needed.
//
// `winner` points to the BidState associated with the winning bid, if there
// is one. If it's not a BidState managed by `this`, it has no effect.
//
// `signals` are the PostAuctionSignals from the auction `this` was a part of.
//
// `top_level_signals` are the PostAuctionSignals of the top-level auction, if
// this is a component auction, and nullopt otherwise.
void TakeDebugReportUrls(
const BidState* winner,
const PostAuctionSignals& signals,
const absl::optional<PostAuctionSignals>& top_level_signals,
std::vector<GURL>& debug_win_report_urls,
std::vector<GURL>& debug_loss_report_urls) {
for (std::unique_ptr<BidState>& bid_state : bid_states_) {
TakeDebugReportUrlsForBidState(bid_state, winner, signals,
top_level_signals, debug_win_report_urls,
debug_loss_report_urls);
}
}
// Returns private aggregation requests, if there are any. Calculate
// bucket/value using `signals` as needed.
//
// `winner` points to the BidState associated with the winning bid, if there
// is one. If it's not a BidState managed by `this`, it has no effect.
//
// `signals` are the PostAuctionSignals from the auction `this` was a part of.
void TakePrivateAggregationRequests(
const BidState* winner,
const BidState* non_kanon_winner,
const PostAuctionSignals& signals,
const absl::optional<PostAuctionSignals>& top_level_signals,
std::map<InterestGroupAuctionReporter::PrivateAggregationKey,
PrivateAggregationRequests>&
private_aggregation_requests_reserved,
std::map<std::string, PrivateAggregationRequests>&
private_aggregation_requests_non_reserved) {
for (std::unique_ptr<BidState>& state : bid_states_) {
TakePrivateAggregationRequestsForBidState(
state, /*is_component_auction=*/auction_->parent_, winner,
non_kanon_winner, signals, top_level_signals,
private_aggregation_requests_reserved,
private_aggregation_requests_non_reserved);
}
}
void NotifyConfigPromisesResolved() {
DCHECK(auction_->config_promises_resolved_);
NotifyConfigDependencyResolved();
}
// Called when promises resolve, or directFromSellerSignalsHeaderAdSlot
// finishes parsing and finding a matching ad slot response.
void NotifyConfigDependencyResolved() {
// If there are no outstanding bids, just do nothing. It's safest to exit
// early in the case that bidder worklet process crashed or failed to fetch
// the necessary script(s) before all config promises were resolved, rather
// than rely on everything handling that case correctly.
if (num_outstanding_bids_ == 0) {
return;
}
MaybeStartCumulativeTimeoutTimer();
for (const auto& bid_state : bid_states_) {
FinishGenerateBidIfReady(bid_state.get());
}
}
std::unique_ptr<Bid> TryToCreateBidFromServerResponse(
InterestGroupAuction::Bid::BidRole bid_role,
double bid,
blink::AdDescriptor ad_descriptor,
std::vector<blink::AdDescriptor> ad_component_descriptors) {
CHECK_EQ(1u, bid_states_.size());
BidState* bid_state = bid_states_[0].get();
bid_state->made_bid = true;
auction_worklet::mojom::KAnonymityBidMode kanon_mode =
auction_->kanon_mode();
bid_state->kanon_keys = ComputeKAnon(bid_state->bidder, kanon_mode);
// Check bid validity.
const blink::InterestGroup& interest_group =
bid_state->bidder->interest_group;
// 1. Ads must be in the interest group (at specified k-anon level)
const blink::InterestGroup::Ad* matching_ad = FindMatchingAd(
*interest_group.ads, bid_state->kanon_keys, interest_group, bid_role,
/*is_component_ad=*/false, ad_descriptor);
if (!matching_ad) {
// Bid render url must match the interest group.
return nullptr;
}
for (const auto& ad_component_descriptor : ad_component_descriptors) {
const blink::InterestGroup::Ad* matching_ad_component = FindMatchingAd(
*interest_group.ad_components, bid_state->kanon_keys, interest_group,
bid_role, /*is_component_ad=*/true, ad_component_descriptor);
if (!matching_ad_component) {
// Bid ad components must match the interest group.
return nullptr;
}
}
// 2. Reporting URLs must be okay
// TODO(1457931): Implement reporting
return std::make_unique<Bid>(
bid_role, matching_ad->metadata.value_or("null"), bid,
/*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt, std::move(ad_descriptor),
std::move(ad_component_descriptors),
/*modeling_signals=*/absl::nullopt,
/*bid_duration=*/base::Seconds(0),
/*bidding_signals_data_version=*/absl::nullopt, matching_ad, bid_state,
auction_);
}
private:
// Sorts by descending priority, also grouping entries within each priority
// band to permit context reuse if the executionMode allows it.
void SortByPriorityAndGroupByJoinOrigin() {
std::sort(bid_states_.begin(), bid_states_.end(),
BidStatesDescByPriorityAndGroupByJoinOrigin());
}
// Applies `size_limit_`, removing the lowest priority interest groups first,
// and then sorts the remaining interest groups.
void ApplySizeLimitAndSort() {
SortByPriorityAndGroupByJoinOrigin();
// Randomize order of interest groups with lowest allowed priority. This
// effectively performs a random sample among interest groups with the
// same priority.
double min_priority = bid_states_[size_limit_ - 1]->calculated_priority;
auto rand_begin = std::lower_bound(bid_states_.begin(), bid_states_.end(),
min_priority, BidStatesDescByPriority());
auto rand_end = std::upper_bound(rand_begin, bid_states_.end(),
min_priority, BidStatesDescByPriority());
base::RandomShuffle(rand_begin, rand_end);
for (size_t i = size_limit_; i < bid_states_.size(); ++i) {
// Need to close pipes explicitly, as the state's GenerateBidClientPipe is
// owned by `generate_bid_client_receiver_set_`, deleting the bid isn't
// sufficient.
CloseBidStatePipes(*bid_states_[i]);
}
auction_->auction_metrics_recorder_->RecordBidsFilteredByPerBuyerLimits(
bid_states_.size() - size_limit_);
bid_states_.resize(size_limit_);
// Restore the origin grouping within lowest priority band among the
// subset that was kept after shuffling.
std::sort(rand_begin, bid_states_.end(),
BidStatesDescByPriorityAndGroupByJoinOrigin());
}
// Called when the `bid_state` BidderWorklet crashes or fails to load.
// Invokes OnGenerateBidCompleteInternal() for the worklet with a failure.
void OnBidderWorkletGenerateBidFatalError(
BidState* bid_state,
AuctionWorkletManager::FatalErrorType fatal_error_type,
const std::vector<std::string>& errors) {
auction_->auction_metrics_recorder_
->RecordBidAbortedByBidderWorkletFatalError();
// Add error(s) directly to error list.
if (fatal_error_type ==
AuctionWorkletManager::FatalErrorType::kWorkletCrash) {
// Ignore default error message in case of crash. Instead, use a more
// specific one.
OnFatalError(
bid_state,
{base::StrCat({bid_state->bidder->interest_group.bidding_url->spec(),
" crashed while trying to run generateBid()."})});
} else {
OnFatalError(bid_state, errors);
}
}
// Called in the case of a fatal error that prevents the `bid_state` worklet
// from bidding.
void OnFatalError(BidState* bid_state, std::vector<std::string> errors) {
auction_->errors_.insert(auction_->errors_.end(),
std::make_move_iterator(errors.begin()),
std::make_move_iterator(errors.end()));
// If waiting on bidding signals, the bidder needs to be removed in the same
// way as if it had a new negative priority value, so reuse that logic. The
// bidder needs to be removed, and the remaining bidders potentially need to
// have the size limit applied and have their generate bid calls resumed, if
// they were waiting on this bidder. Therefore, can't just call
// OnGenerateBidCompleteInternal().
if (!bid_state->bidding_signals_received) {
OnBiddingSignalsReceivedInternal(bid_state,
/*new_priority=*/-1,
base::OnceClosure());
return;
}
// Otherwise call OnGenerateBidCompleteInternal() directly to complete the
// bid. This will also result in closing pipes. If
// `enable_bidding_signals_prioritization_` is true, the closed pipe will be
// noticed, and it will be removed before applying the priority filter.
OnGenerateBidCompleteInternal(
bid_state,
/*mojo_bid=*/auction_worklet::mojom::BidderWorkletBidPtr(),
/*mojo_kanon_bid=*/
auction_worklet::mojom::BidderWorkletKAnonEnforcedBidPtr(),
/*bidding_signals_data_version=*/0,
/*has_bidding_signals_data_version=*/false,
/*debug_loss_report_url=*/absl::nullopt,
/*debug_win_report_url=*/absl::nullopt,
/*set_priority=*/0,
/*has_set_priority=*/false,
/*update_priority_signals_overrides=*/{},
/*pa_requests=*/{},
/*non_kanon_pa_requests=*/{},
/*reject_reason=*/auction_worklet::mojom::RejectReason::kNotAvailable,
/*errors=*/{});
}
base::flat_map<std::string, bool> ComputeKAnon(
const SingleStorageInterestGroup& storage_interest_group,
auction_worklet::mojom::KAnonymityBidMode kanon_mode) {
if (kanon_mode == auction_worklet::mojom::KAnonymityBidMode::kNone) {
return {};
}
// k-anon cache is always checked against the same time, to avoid weird
// behavior of validity changing in the middle of the auction.
base::Time start_time = auction_->auction_start_time_;
std::vector<std::pair<std::string, bool>> kanon_entries;
for (const auto& ad_kanon : storage_interest_group->bidding_ads_kanon) {
if (IsKAnonymous(ad_kanon, start_time)) {
kanon_entries.emplace_back(ad_kanon.key, true);
}
}
for (const auto& component_ad_kanon :
storage_interest_group->component_ads_kanon) {
if (IsKAnonymous(component_ad_kanon, start_time)) {
kanon_entries.emplace_back(component_ad_kanon.key, true);
}
}
return base::flat_map<std::string, bool>(std::move(kanon_entries));
}
// Invoked whenever the AuctionWorkletManager has provided a BidderWorket
// for the bidder identified by `bid_state`. Starts generating a bid.
void OnBidderWorkletReceived(BidState* bid_state) {
if (!bidder_process_received_) {
// All bidder worklets are expected to be loaded in the same process, so
// as soon as any worklet has been received, can set this to true.
bidder_process_received_ = true;
MaybeStartCumulativeTimeoutTimer();
}
const blink::InterestGroup& interest_group =
bid_state->bidder->interest_group;
bid_state->BeginTracing();
TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("fledge", "bidder_worklet_generate_bid",
*bid_state->trace_id);
mojo::PendingAssociatedRemote<auction_worklet::mojom::GenerateBidClient>
pending_remote;
bid_state->generate_bid_client_receiver_id =
generate_bid_client_receiver_set_.Add(
this, pending_remote.InitWithNewEndpointAndPassReceiver(),
bid_state);
auction_worklet::mojom::KAnonymityBidMode kanon_mode =
auction_->kanon_mode();
bid_state->kanon_keys = ComputeKAnon(bid_state->bidder, kanon_mode);
SubresourceUrlBuilder* url_builder =
auction_->SubresourceUrlBuilderIfReady();
if (url_builder) {
bid_state->worklet_handle->AuthorizeSubresourceUrls(*url_builder);
bid_state->handled_direct_from_seller_signals_in_begin_generate_bid =
true;
}
bid_state->worklet_handle->GetBidderWorklet()->BeginGenerateBid(
auction_worklet::mojom::BidderWorkletNonSharedParams::New(
interest_group.name,
interest_group.enable_bidding_signals_prioritization,
interest_group.priority_vector, interest_group.execution_mode,
interest_group.update_url,
interest_group.trusted_bidding_signals_keys,
interest_group.user_bidding_signals, interest_group.ads,
interest_group.ad_components,
KAnonKeysToMojom(bid_state->kanon_keys)),
kanon_mode, bid_state->bidder->joining_origin,
GetDirectFromSellerPerBuyerSignals(
url_builder, bid_state->bidder->interest_group.owner),
GetDirectFromSellerAuctionSignals(url_builder),
auction_->config_->seller,
auction_->parent_ ? auction_->parent_->config_->seller
: absl::optional<url::Origin>(),
(base::Time::Now() - bid_state->bidder->join_time)
.RoundToMultiple(base::Milliseconds(100)),
bid_state->bidder->bidding_browser_signals.Clone(),
auction_->auction_start_time_, auction_->RequestedAdSize(),
*bid_state->trace_id, std::move(pending_remote),
bid_state->bid_finalizer.BindNewEndpointAndPassReceiver());
// TODO(morlovich): This should arguably be merged into BeginGenerateBid
// for footprint; check how testable that would be.
if (bid_state->send_pending_trusted_signals_after_generate_bid) {
bid_state->worklet_handle->GetBidderWorklet()
->SendPendingSignalsRequests();
}
FinishGenerateBidIfReady(bid_state);
}
void FinishGenerateBidIfReady(BidState* bid_state) {
if (!auction_->config_promises_resolved_ ||
auction_->direct_from_seller_signals_header_ad_slot_pending_) {
return;
}
if (!bid_state->bid_finalizer.is_bound()) {
// This can happen if the promise resolves while the worklet process is
// still being launched.
return;
}
SubresourceUrlBuilder* url_builder =
auction_->SubresourceUrlBuilderIfReady();
if (url_builder) {
if (bid_state->handled_direct_from_seller_signals_in_begin_generate_bid) {
// Don't provide direct-from-seller info to FinishGenerateBid below
// since we already provided it to BeginGenerateBid.
url_builder = nullptr;
} else {
bid_state->worklet_handle->AuthorizeSubresourceUrls(*url_builder);
}
}
bid_state->bid_finalizer->FinishGenerateBid(
auction_->config_->non_shared_params.auction_signals.value(),
GetPerBuyerSignals(*auction_->config_,
bid_state->bidder->interest_group.owner),
PerBuyerTimeout(owner_, *auction_->config_),
PerBuyerCurrency(owner_, *auction_->config_),
GetDirectFromSellerPerBuyerSignals(
url_builder, bid_state->bidder->interest_group.owner),
GetDirectFromSellerPerBuyerSignalsHeaderAdSlot(
*auction_->direct_from_seller_signals_header_ad_slot(),
bid_state->bidder->interest_group.owner),
GetDirectFromSellerAuctionSignals(url_builder),
GetDirectFromSellerAuctionSignalsHeaderAdSlot(
*auction_->direct_from_seller_signals_header_ad_slot()));
bid_state->bid_finalizer.reset();
}
// Invoked when OnBiddingSignalsReceived() has been called for `state`, or
// with a negative priority when the worklet process has an error, or the
// buyer reaches their cumulative timeout, and is still waiting on the
// OnBiddingSignalsReceived() invocation.
void OnBiddingSignalsReceivedInternal(
BidState* state,
absl::optional<double> new_priority,
base::OnceClosure resume_generate_bid_callback) {
DCHECK(!state->bidding_signals_received);
DCHECK_GT(num_outstanding_bids_, 0);
DCHECK_GT(num_outstanding_bidding_signals_received_calls_, 0);
// `resume_generate_bid_callback` must be non-null except when invoked with
// a negative `net_priority` on worklet error.
DCHECK(resume_generate_bid_callback || *new_priority < 0);
state->bidding_signals_received = true;
--num_outstanding_bidding_signals_received_calls_;
// If `new_priority` has a value and is negative, need to record the bidder
// as no longer participating in the auction and cancel bid generation.
bool bid_filtered = new_priority.has_value() && *new_priority < 0;
UMA_HISTOGRAM_BOOLEAN("Ads.InterestGroup.Auction.BidFiltered",
bid_filtered);
if (bid_filtered) {
// Record if there are other bidders, as if there are not, the next call
// may delete `this`.
bool other_bidders = (num_outstanding_bids_ > 1);
// If the result of applying the filter is negative, complete the bid
// with OnGenerateBidCompleteInternal(), which will close the relevant
// pipes and abort bid generation.
OnGenerateBidCompleteInternal(
state, /*mojo_bid=*/auction_worklet::mojom::BidderWorkletBidPtr(),
/*mojo_kanon_bid=*/
auction_worklet::mojom::BidderWorkletKAnonEnforcedBidPtr(),
/*bidding_signals_data_version=*/0,
/*has_bidding_signals_data_version=*/false,
/*debug_loss_report_url=*/absl::nullopt,
/*debug_win_report_url=*/absl::nullopt,
/*set_priority=*/0,
/*has_set_priority=*/false,
/*update_priority_signals_overrides=*/{},
/*pa_requests=*/{},
/*non_kanon_pa_requests=*/{},
/*reject_reason=*/auction_worklet::mojom::RejectReason::kNotAvailable,
/*errors=*/{});
// If this was the last bidder, and it was filtered out, there's nothing
// else to do, and `this` may have already been deleted.
if (!other_bidders) {
return;
}
// If bidding_signals_prioritization is not enabled, there's also
// nothing else to do - no other bidders were blocked on the bidder's
// OnBiddingSignalsReceived() call.
if (!enable_bidding_signals_prioritization_) {
return;
}
} else {
if (new_priority.has_value()) {
state->calculated_priority = *new_priority;
}
// Otherwise, invoke the callback to proceed to generate a bid, if don't
// need to prioritize / filter based on number of interest groups.
if (!enable_bidding_signals_prioritization_) {
std::move(resume_generate_bid_callback).Run();
return;
}
state->resume_generate_bid_callback =
std::move(resume_generate_bid_callback);
}
// Check if there are any outstanding OnBiddingSignalsReceived() calls. If
// so, need to sort interest groups by priority resume pending generate bid
// calls.
DCHECK(enable_bidding_signals_prioritization_);
if (num_outstanding_bidding_signals_received_calls_ > 0) {
return;
}
// Remove Bid states that were filtered out due to having negative new
// priorities, as ApplySizeLimitAndSort() assumes all bidders are still
// potentially capable of generating bids. Do these all at once to avoid
// repeatedly searching for bid states that had negative priority vector
// multiplication results, each time a priority vector is received.
for (size_t i = 0; i < bid_states_.size();) {
// Removing a bid is guaranteed to destroy the worklet handle, though not
// necessarily the `resume_generate_bid_callback` (in particular,
// OnBidderWorkletGenerateBidFatalError() calls OnGenerateBidInternal() if
// a worklet with a `resume_generate_bid_callback` already set crashes,
// but does not clear `resume_generate_bid_callback`, since doing so
// directly without closing the pipe first will DCHECK).
if (!bid_states_[i]->worklet_handle) {
// The GenerateBidClient pipe should also have been closed.
DCHECK(!bid_states_[i]->generate_bid_client_receiver_id);
// std::swap() instead of std::move() because self-move isn't guaranteed
// to work.
std::swap(bid_states_[i], bid_states_.back());
bid_states_.pop_back();
size_limit_ = std::min(size_limit_, bid_states_.size());
continue;
}
DCHECK(bid_states_[i]->resume_generate_bid_callback);
++i;
}
// The above loop should have deleted any bid states not accounted for in
// `num_outstanding_bids_`.
DCHECK_EQ(static_cast<size_t>(num_outstanding_bids_), bid_states_.size());
ApplySizeLimitAndSort();
// Update `num_outstanding_bids_` to reflect the remaining number of pending
// bids, after applying the size limit.
num_outstanding_bids_ = bid_states_.size();
// Let all generate bid calls proceed.
for (auto& pending_state : bid_states_) {
std::move(pending_state->resume_generate_bid_callback).Run();
}
}
// Called once a bid has been generated, or has failed to be generated.
// Releases the BidderWorklet handle and instructs the SellerWorklet to
// start scoring the bid, if there is one.
void OnGenerateBidCompleteInternal(
BidState* state,
auction_worklet::mojom::BidderWorkletBidPtr mojo_bid,
auction_worklet::mojom::BidderWorkletKAnonEnforcedBidPtr mojo_kanon_bid,
uint32_t bidding_signals_data_version,
bool has_bidding_signals_data_version,
const absl::optional<GURL>& debug_loss_report_url,
const absl::optional<GURL>& debug_win_report_url,
double set_priority,
bool has_set_priority,
base::flat_map<std::string,
auction_worklet::mojom::PrioritySignalsDoublePtr>
update_priority_signals_overrides,
PrivateAggregationRequests pa_requests,
PrivateAggregationRequests non_kanon_pa_requests,
auction_worklet::mojom::RejectReason reject_reason,
const std::vector<std::string>& errors) {
DCHECK(!state->made_bid);
DCHECK_GT(num_outstanding_bids_, 0);
TRACE_EVENT_NESTABLE_ASYNC_END0("fledge", "bidder_worklet_generate_bid",
*state->trace_id);
const blink::InterestGroup& interest_group = state->bidder->interest_group;
absl::optional<uint32_t> maybe_bidding_signals_data_version;
if (has_bidding_signals_data_version) {
maybe_bidding_signals_data_version = bidding_signals_data_version;
}
if (has_set_priority) {
auction_->interest_group_manager_->SetInterestGroupPriority(
blink::InterestGroupKey(interest_group.owner, interest_group.name),
set_priority);
}
if (!update_priority_signals_overrides.empty()) {
// Reject infinite values. The worklet code should prevent this, but the
// process may be compromised. This is largely preventing the owner from
// messing up its own prioritization function, but there could be issues
// around serializing infinite values to persist to disk as well.
//
// Note that the data received here has no effect on the result of the
// auction, so just reject the data and continue with the auction to keep
// the code simple.
if (base::ranges::any_of(
update_priority_signals_overrides, [](const auto& pair) {
return pair.second && !std::isfinite(pair.second->value);
})) {
generate_bid_client_receiver_set_.ReportBadMessage(
"Invalid priority signals overrides");
} else {
auction_->interest_group_manager_->UpdateInterestGroupPriorityOverrides(
blink::InterestGroupKey(interest_group.owner, interest_group.name),
std::move(update_priority_signals_overrides));
}
}
// Validate `mojo_kanon_bid` coming in from the less-trusted worklet
// process; k-anonymity itself will be checked by TryToCreateBid.
if (mojo_kanon_bid) {
if (auction_->kanon_mode_ ==
auction_worklet::mojom::KAnonymityBidMode::kNone) {
generate_bid_client_receiver_set_.ReportBadMessage(
"Received k-anon bid data when not considering k-anon");
mojo_kanon_bid.reset();
} else if (mojo_bid &&
IsKAnon(state->kanon_keys, interest_group, mojo_bid.get())) {
if (!mojo_kanon_bid->is_same_as_non_enforced()) {
generate_bid_client_receiver_set_.ReportBadMessage(
"Received different k-anon bid when unenforced bid already "
"k-anon");
mojo_kanon_bid = auction_worklet::mojom::
BidderWorkletKAnonEnforcedBid::NewSameAsNonEnforced(nullptr);
}
}
}
// The mojom API declaration should ensure none of these are null.
DCHECK(base::ranges::none_of(
pa_requests,
[](const auction_worklet::mojom::PrivateAggregationRequestPtr&
request_ptr) { return request_ptr.is_null(); }));
DCHECK(base::ranges::none_of(
non_kanon_pa_requests,
[](const auction_worklet::mojom::PrivateAggregationRequestPtr&
request_ptr) { return request_ptr.is_null(); }));
auction_->MaybeLogPrivateAggregationWebFeatures(pa_requests);
if (!pa_requests.empty()) {
BidState::PrivateAggregationPhaseKey agg_key = {
interest_group.owner, PrivateAggregationPhase::kBidder,
interest_group.aggregation_coordinator_origin};
PrivateAggregationRequests& pa_requests_for_bidder =
state->private_aggregation_requests[std::move(agg_key)];
pa_requests_for_bidder.insert(pa_requests_for_bidder.end(),
std::move_iterator(pa_requests.begin()),
std::move_iterator(pa_requests.end()));
}
if (!non_kanon_pa_requests.empty()) {
PrivateAggregationRequests& non_kanon_pa_requests_for_bidder =
state->non_kanon_private_aggregation_requests;
non_kanon_pa_requests_for_bidder.insert(
non_kanon_pa_requests_for_bidder.end(),
std::move_iterator(non_kanon_pa_requests.begin()),
std::move_iterator(non_kanon_pa_requests.end()));
}
auction_->errors_.insert(auction_->errors_.end(), errors.begin(),
errors.end());
// Ignore invalid bids.
std::unique_ptr<Bid> bid;
std::string ad_metadata;
if (reject_reason != auction_worklet::mojom::RejectReason::kNotAvailable &&
reject_reason !=
auction_worklet::mojom::RejectReason::kWrongGenerateBidCurrency) {
// Only possible reject reason from generateBid(), besides the default
// "not available", is "wrong generate bid currency".
mojo_bid.reset();
mojo_kanon_bid.reset();
state->reject_reason =
auction_worklet::mojom::RejectReason::kNotAvailable;
generate_bid_client_receiver_set_.ReportBadMessage(
"Invalid bid reject_reason");
} else {
state->reject_reason = reject_reason;
}
// `mojo_bid` is null if the worklet doesn't bid, or if the bidder worklet
// fails to load / crashes.
if (mojo_bid) {
// It's possible that k-anon enforced bid is the same as one with out
// enforcement, in which case we make sure to only run ScoreBid once.
Bid::BidRole role = Bid::BidRole::kUnenforcedKAnon;
if (mojo_kanon_bid) {
if (mojo_kanon_bid->is_same_as_non_enforced()) {
role = Bid::BidRole::kBothKAnonModes;
auction_->auction_metrics_recorder_
->RecordInterestGroupWithSameBidForKAnonAndNonKAnon();
} else {
auction_->auction_metrics_recorder_
->RecordInterestGroupWithSeparateBidsForKAnonAndNonKAnon();
}
} else {
auction_->auction_metrics_recorder_
->RecordInterestGroupWithOnlyNonKAnonBid();
}
bid = TryToCreateBid(role, std::move(mojo_bid), *state,
maybe_bidding_signals_data_version,
debug_loss_report_url, debug_win_report_url);
if (bid) {
state->bidder_debug_loss_report_url = debug_loss_report_url;
}
} else {
// Bidders who do not bid are allowed to get loss report.
state->bidder_debug_loss_report_url = debug_loss_report_url;
auction_->auction_metrics_recorder_->RecordInterestGroupWithNoBids();
}
std::unique_ptr<Bid> kanon_bid;
if (mojo_kanon_bid && !mojo_kanon_bid->is_same_as_non_enforced()) {
kanon_bid = TryToCreateBid(Bid::BidRole::kEnforcedKAnon,
std::move(mojo_kanon_bid->get_bid()), *state,
maybe_bidding_signals_data_version,
/*debug_loss_report_url=*/absl::nullopt,
/*debug_win_report_url=*/absl::nullopt);
}
// Release the worklet. If it wins the auction, it will be requested again
// to invoke its ReportWin() method.
CloseBidStatePipes(*state);
if (!bid && !kanon_bid) {
if (state->trace_id.has_value()) {
// Might not have started it if we timed out before worklet received.
state->EndTracing();
}
} else {
state->bidder_debug_win_report_url = debug_win_report_url;
state->made_bid = true;
if (bid) {
auction_->ScoreBidIfReady(std::move(bid));
}
if (kanon_bid) {
state->BeginTracingKAnonScoring();
auction_->ScoreBidIfReady(std::move(kanon_bid));
}
}
--num_outstanding_bids_;
if (num_outstanding_bids_ == 0) {
DCHECK_EQ(num_outstanding_bidding_signals_received_calls_, 0);
// Pipes should already be closed at this point, but the
// `cumulative_buyer_timeout_timer_` needs to be stopped if it's running,
// and it's safest to keep all logic to stop everything `this` may be
// doing in one place.
ClosePipes();
auction_->OnScoringDependencyDone();
}
}
void MaybeStartCumulativeTimeoutTimer() {
// This should only be called when there are outstanding bids.
DCHECK_GT(num_outstanding_bids_, 0);
// Do nothing if still waiting on the seller to provide more of the
// AuctionConfig, or directFromSellerSignalsHeaderAdSlot is still
// parsing and finding a matching ad slot response, or waiting on a process
// to be assigned (which would mean that this may be waiting behind other
// buyers).
if (!auction_->config_promises_resolved_ ||
auction_->direct_from_seller_signals_header_ad_slot_pending_ ||
!bidder_process_received_) {
return;
}
DCHECK(!cumulative_buyer_timeout_timer_.IsRunning());
// Get cumulative buyer timeout. Note that this must be done after the
// `config_promises_resolved_` check above.
absl::optional<base::TimeDelta> cumulative_buyer_timeout =
PerBuyerCumulativeTimeout(owner_, *auction_->config_);
// Nothing to do if there's no cumulative timeout.
if (!cumulative_buyer_timeout) {
return;
}
cumulative_buyer_timeout_timer_.Start(
FROM_HERE, *cumulative_buyer_timeout,
base::BindOnce(&BuyerHelper::OnTimeout, base::Unretained(this)));
}
// Called when the `cumulative_buyer_timeout_timer_` expires.
void OnTimeout() {
// If there are no outstanding bids, then the timer should not still be
// running.
DCHECK_GT(num_outstanding_bids_, 0);
// Assemble a list of interest groups that haven't bid yet - have to do
// this, since calling OnGenerateBidCompleteInternal() on the last
// incomplete bid may delete `this`, if it ends the auction.
std::list<BidState*> pending_bids;
for (auto& bid_state : bid_states_) {
if (!bid_state->worklet_handle) {
continue;
}
// Put the IGs that have received signals first, since cancelling the last
// bid that has not received signals could cause a bid that has received
// signals to start running Javascript.
if (bid_state->bidding_signals_received) {
pending_bids.push_front(bid_state.get());
} else {
pending_bids.push_back(bid_state.get());
}
}
auction_->auction_metrics_recorder_
->RecordBidsAbortedByBuyerCumulativeTimeout(pending_bids.size());
for (auto* pending_bid : pending_bids) {
// We specifically include timeouts in this metric.
auction_->auction_metrics_recorder_->RecordBidForOneInterestGroupLatency(
base::TimeTicks::Now() - start_generating_bids_time_);
// Fail bids individually, with errors. This does potentially do extra
// work over just failing the entire auction directly, but ensures there's
// a single failure path, reducing the chance of future breakages.
OnFatalError(
pending_bid, /*errors=*/{base::StrCat(
{pending_bid->bidder->interest_group.bidding_url->spec(),
" perBuyerCumulativeTimeout exceeded during bid generation."})});
}
}
// Validates that `mojo_bid` is valid and, if it is, creates a Bid
// corresponding to it, consuming it. Returns nullptr and calls
// ReportBadMessage() if it's not valid. Does not mutate `bid_state`, but
// the returned Bid has a non-const pointer to it.
std::unique_ptr<InterestGroupAuction::Bid> TryToCreateBid(
InterestGroupAuction::Bid::BidRole bid_role,
auction_worklet::mojom::BidderWorkletBidPtr mojo_bid,
BidState& bid_state,
const absl::optional<uint32_t>& bidding_signals_data_version,
const absl::optional<GURL>& debug_loss_report_url,
const absl::optional<GURL>& debug_win_report_url) {
// We record the bid duration even if the bid is invalid to avoid bias.
auction_->auction_metrics_recorder_->RecordGenerateSingleBidLatency(
mojo_bid->bid_duration);
if (!IsValidBid(mojo_bid->bid)) {
generate_bid_client_receiver_set_.ReportBadMessage("Invalid bid value");
return nullptr;
}
if (mojo_bid->bid_duration.is_negative()) {
generate_bid_client_receiver_set_.ReportBadMessage(
"Invalid bid duration");
return nullptr;
}
if (!blink::VerifyAdCurrencyCode(
PerBuyerCurrency(owner_, *auction_->config_),
mojo_bid->bid_currency)) {
generate_bid_client_receiver_set_.ReportBadMessage(
"Invalid bid currency");
return nullptr;
}
const blink::InterestGroup& interest_group =
bid_state.bidder->interest_group;
const blink::InterestGroup::Ad* matching_ad = FindMatchingAd(
*interest_group.ads, bid_state.kanon_keys, interest_group, bid_role,
/*is_component_ad=*/false, mojo_bid->ad_descriptor);
if (!matching_ad) {
generate_bid_client_receiver_set_.ReportBadMessage(
"Bid render ad must have a valid URL and size (if specified)");
return nullptr;
}
// If the matching ad does not have size specified, the bid to be created
// should not have size specified to fall back to old behavior.
blink::AdDescriptor ad_descriptor(
mojo_bid->ad_descriptor.url,
matching_ad->size_group ? mojo_bid->ad_descriptor.size : absl::nullopt);
// Validate `ad_component` URLs, if present.
std::vector<blink::AdDescriptor> ad_component_descriptors;
if (mojo_bid->ad_component_descriptors) {
// Only InterestGroups with ad components should return bids with ad
// components.
if (!interest_group.ad_components) {
generate_bid_client_receiver_set_.ReportBadMessage(
"Unexpected non-null ad component list");
return nullptr;
}
if (mojo_bid->ad_component_descriptors->size() >
blink::kMaxAdAuctionAdComponents) {
generate_bid_client_receiver_set_.ReportBadMessage(
"Too many ad component URLs");
return nullptr;
}
// Validate each ad component URL is valid and appears in the interest
// group's `ad_components` field.
for (const blink::AdDescriptor& ad_component_descriptor :
*mojo_bid->ad_component_descriptors) {
const blink::InterestGroup::Ad* matching_ad_component = FindMatchingAd(
*interest_group.ad_components, bid_state.kanon_keys, interest_group,
bid_role, /*is_component_ad=*/true, ad_component_descriptor);
if (!matching_ad_component) {
generate_bid_client_receiver_set_.ReportBadMessage(
"Bid ad component must have a valid URL and size (if specified)");
return nullptr;
}
// If the matching ad does not have size specified, the bid to be
// created should not have size specified to fall back to old behavior.
ad_component_descriptors.emplace_back(ad_component_descriptor.url,
matching_ad_component->size_group
? ad_component_descriptor.size
: absl::nullopt);
}
}
// Validate `debug_loss_report_url` and `debug_win_report_url`, if present.
if (debug_loss_report_url.has_value() &&
!IsUrlValid(debug_loss_report_url.value())) {
generate_bid_client_receiver_set_.ReportBadMessage(
"Invalid bidder debugging loss report URL");
return nullptr;
}
if (debug_win_report_url.has_value() &&
!IsUrlValid(debug_win_report_url.value())) {
generate_bid_client_receiver_set_.ReportBadMessage(
"Invalid bidder debugging win report URL");
return nullptr;
}
return std::make_unique<Bid>(
bid_role, std::move(mojo_bid->ad), mojo_bid->bid,
std::move(mojo_bid->bid_currency), mojo_bid->ad_cost,
std::move(ad_descriptor), std::move(ad_component_descriptors),
std::move(mojo_bid->modeling_signals), mojo_bid->bid_duration,
bidding_signals_data_version, matching_ad, &bid_state, auction_);
}
// Close all Mojo pipes associated with `state`.
void CloseBidStatePipes(BidState& state) {
state.worklet_handle.reset();
if (state.generate_bid_client_receiver_id) {
generate_bid_client_receiver_set_.Remove(
*state.generate_bid_client_receiver_id);
state.generate_bid_client_receiver_id.reset();
state.bid_finalizer.reset();
}
}
size_t size_limit_;
const raw_ptr<InterestGroupAuction> auction_;
const url::Origin owner_;
// State of loaded interest groups owned by `owner_`. Use unique_ptrs so that
// pointers aren't invalidated by sorting / deleting BidStates.
std::vector<std::unique_ptr<BidState>> bid_states_;
// Per-BidState receivers. These can never be null. Uses unique_ptrs so that
// existing pointers aren't invalidated by sorting / deleting BidStates.
mojo::AssociatedReceiverSet<auction_worklet::mojom::GenerateBidClient,
BidState*>
generate_bid_client_receiver_set_;
// Set to true once a single bidder worklet has been received (and thus, since
// all bidder worklets managed by a BuyerHelper use the same process, `this`
// is no longer blocked waiting on other bidders to complete).
bool bidder_process_received_ = false;
// Timer for applying the perBidderCumulativeTimeout, if one is applicable.
// Starts once `bidder_process_received_`,
// `auction_->config_promises_resolved_` are true, and
// `auction_->direct_from_seller_signals_header_ad_slot_pending_` is false,
// if `cumulative_buyer_timeout_` is not nullopt.
base::OneShotTimer cumulative_buyer_timeout_timer_;
int num_outstanding_bidding_signals_received_calls_ = 0;
int num_outstanding_bids_ = 0;
// Records the time at which StartGeneratingBids was called for UKM.
base::TimeTicks start_generating_bids_time_;
// True if any interest group owned by `owner_` participating in this auction
// has `use_biddings_signals_prioritization` set to true. When this is true,
// all GenerateBid() calls will be deferred until OnBiddingSignalsReceived()
// has been invoked for all bidders (or they've failed to generate bids due to
// errors).
//
// TODO(mmenke): Could only set this to true if the number of bidders exceeds
// the per-buyer limit as well, and only the `priority_vector` as a filter for
// buyers with `use_biddings_signals_prioritization` set to true, as a small
// performance optimization.
bool enable_bidding_signals_prioritization_ = false;
base::WeakPtrFactory<BuyerHelper> weak_ptr_factory_{this};
};
InterestGroupAuction::InterestGroupAuction(
auction_worklet::mojom::KAnonymityBidMode kanon_mode,
const blink::AuctionConfig* config,
const InterestGroupAuction* parent,
AuctionWorkletManager* auction_worklet_manager,
AuctionNonceManager* auction_nonce_manager,
InterestGroupManagerImpl* interest_group_manager,
AuctionMetricsRecorder* auction_metrics_recorder,
AdAuctionPageData* ad_auction_page_data,
base::Time auction_start_time,
IsInterestGroupApiAllowedCallback is_interest_group_api_allowed_callback,
base::RepeatingCallback<
void(const PrivateAggregationRequests& private_aggregation_requests)>
maybe_log_private_aggregation_web_features_callback)
: trace_id_(base::trace_event::GetNextGlobalTraceId()),
kanon_mode_(kanon_mode),
auction_worklet_manager_(auction_worklet_manager),
auction_nonce_manager_(auction_nonce_manager),
interest_group_manager_(interest_group_manager),
auction_metrics_recorder_(auction_metrics_recorder),
config_(config),
config_promises_resolved_(config_->NumPromises() == 0),
parent_(parent),
negative_targeter_(std::make_unique<AdAuctionNegativeTargeter>()),
auction_start_time_(auction_start_time),
creation_time_(base::TimeTicks::Now()),
is_interest_group_api_allowed_callback_(
std::move(is_interest_group_api_allowed_callback)),
maybe_log_private_aggregation_web_features_callback_(
std::move(maybe_log_private_aggregation_web_features_callback)),
data_decoder_(ad_auction_page_data->GetDecoderFor(config->seller)) {
DCHECK(is_interest_group_api_allowed_callback_);
TRACE_EVENT_NESTABLE_ASYNC_BEGIN1("fledge", "auction", *trace_id_,
"decision_logic_url",
config_->decision_logic_url);
uint32_t child_pos = 0;
for (const auto& component_auction_config :
config->non_shared_params.component_auctions) {
// Nested component auctions are not supported.
DCHECK(!parent_);
component_auctions_.emplace(
child_pos, std::make_unique<InterestGroupAuction>(
kanon_mode_, &component_auction_config, /*parent=*/this,
auction_worklet_manager, auction_nonce_manager,
interest_group_manager, auction_metrics_recorder_,
ad_auction_page_data, auction_start_time_,
is_interest_group_api_allowed_callback_,
maybe_log_private_aggregation_web_features_callback_));
++child_pos;
}
if (!parent_) {
auction_metrics_recorder_->SetKAnonymityBidMode(kanon_mode);
auction_metrics_recorder_->SetNumConfigPromises(config_->NumPromises());
}
}
InterestGroupAuction::~InterestGroupAuction() {
if (trace_id_.has_value()) {
TRACE_EVENT_NESTABLE_ASYNC_END0("fledge", "auction", *trace_id_);
}
if (!final_auction_result_) {
final_auction_result_ = AuctionResult::kAborted;
}
std::string uma_prefix = "Ads.InterestGroup.Auction.";
if (is_server_auction_) {
uma_prefix = "Ads.InterestGroup.ServerAuction.";
}
// TODO(mmenke): Record histograms for component auctions.
if (!parent_) {
base::UmaHistogramEnumeration(uma_prefix + "Result",
*final_auction_result_);
if (HasNonKAnonWinner()) {
base::UmaHistogramBoolean(uma_prefix + "NonKAnonWinnerIsKAnon",
NonKAnonWinnerIsKAnon());
}
// Only record time of full auctions and aborts.
base::TimeTicks now = base::TimeTicks::Now();
switch (*final_auction_result_) {
case AuctionResult::kAborted:
base::UmaHistogramMediumTimes(uma_prefix + "AbortTime",
now - creation_time_);
break;
case AuctionResult::kNoBids:
case AuctionResult::kAllBidsRejected:
base::UmaHistogramMediumTimes(uma_prefix + "CompletedWithoutWinnerTime",
now - creation_time_);
if (is_server_auction_) {
base::UmaHistogramMediumTimes(
"Ads.InterestGroup.ServerAuction.EndToEndTimeNoWinner",
now - get_ad_auction_data_start_time_);
}
break;
case AuctionResult::kSuccess:
base::UmaHistogramMediumTimes(uma_prefix + "AuctionWithWinnerTime",
now - creation_time_);
if (is_server_auction_) {
base::UmaHistogramMediumTimes(
"Ads.InterestGroup.ServerAuction.EndToEndTime",
now - get_ad_auction_data_start_time_);
}
break;
default:
break;
}
// Last UKM we record for this auction. This finalizes and records the
// AdsInterestGroup_AuctionLatency entry. Any further interactions with
// auction_metrics_recorder_ will likely cause a CHECK-fail.
auction_metrics_recorder_->OnAuctionEnd(*final_auction_result_);
}
}
void InterestGroupAuction::StartLoadInterestGroupsPhase(
AuctionPhaseCompletionCallback load_interest_groups_phase_callback) {
DCHECK(load_interest_groups_phase_callback);
DCHECK(buyer_helpers_.empty());
DCHECK(!load_interest_groups_phase_callback_);
DCHECK(!bidding_and_scoring_phase_callback_);
DCHECK(!final_auction_result_);
DCHECK_EQ(num_pending_loads_, 0u);
TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("fledge", "load_groups_phase", *trace_id_);
load_interest_groups_phase_callback_ =
std::move(load_interest_groups_phase_callback);
// If the seller can't participate in the auction, fail the auction.
if (!is_interest_group_api_allowed_callback_.Run(
ContentBrowserClient::InterestGroupApiOperation::kSell,
config_->seller)) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&InterestGroupAuction::OnStartLoadInterestGroupsPhaseComplete,
weak_ptr_factory_.GetWeakPtr(), AuctionResult::kSellerRejected));
return;
}
if (config_->non_shared_params.auction_nonce) {
if (!auction_nonce_manager_->ClaimAuctionNonceIfAvailable(
static_cast<AuctionNonce>(
*config_->non_shared_params.auction_nonce))) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&InterestGroupAuction::OnStartLoadInterestGroupsPhaseComplete,
weak_ptr_factory_.GetWeakPtr(),
AuctionResult::kInvalidAuctionNonce));
return;
}
}
for (auto component_auction = component_auctions_.begin();
component_auction != component_auctions_.end(); ++component_auction) {
component_auction->second->StartLoadInterestGroupsPhase(
base::BindOnce(&InterestGroupAuction::OnComponentInterestGroupsRead,
weak_ptr_factory_.GetWeakPtr(), component_auction));
++num_pending_loads_;
}
if (config_->non_shared_params.interest_group_buyers) {
for (const auto& buyer :
*config_->non_shared_params.interest_group_buyers) {
if (!is_interest_group_api_allowed_callback_.Run(
ContentBrowserClient::InterestGroupApiOperation::kBuy, buyer)) {
continue;
}
interest_group_manager_->GetInterestGroupsForOwner(
buyer, base::BindOnce(&InterestGroupAuction::OnInterestGroupRead,
weak_ptr_factory_.GetWeakPtr()));
++num_pending_loads_;
}
}
if (num_pending_loads_ == 0) {
// There is nothing to load. We move on to the bidding and scoring phase
// anyway, since it may need to wait for config promises to be resolved
// (and checked) and also potentially deal with additional_bids.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&InterestGroupAuction::OnStartLoadInterestGroupsPhaseComplete,
weak_ptr_factory_.GetWeakPtr(), AuctionResult::kSuccess));
}
}
void InterestGroupAuction::StartBiddingAndScoringPhase(
base::OnceClosure on_seller_receiver_callback,
AuctionPhaseCompletionCallback bidding_and_scoring_phase_callback) {
DCHECK(bidding_and_scoring_phase_callback);
DCHECK(!on_seller_receiver_callback_);
DCHECK(!load_interest_groups_phase_callback_);
DCHECK(!bidding_and_scoring_phase_callback_);
DCHECK(!final_auction_result_);
DCHECK(!non_kanon_enforced_auction_leader_.top_bid);
DCHECK(!kanon_enforced_auction_leader_.top_bid);
DCHECK_EQ(pending_component_seller_worklet_requests_, 0u);
DCHECK_EQ(bidding_and_scoring_phase_state_, PhaseState::kBefore);
bidding_and_scoring_phase_state_ = PhaseState::kDuring;
TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("fledge", "bidding_and_scoring_phase",
*trace_id_);
on_seller_receiver_callback_ = std::move(on_seller_receiver_callback);
bidding_and_scoring_phase_callback_ =
std::move(bidding_and_scoring_phase_callback);
bidding_and_scoring_phase_start_time_ = base::TimeTicks::Now();
CHECK_EQ(num_scoring_dependencies_, 0);
num_scoring_dependencies_ =
buyer_helpers_.size() + component_auctions_.size();
// Also wait for config to resolve.
if (!config_promises_resolved_) {
++num_scoring_dependencies_;
}
// Also wait for directFromSellerSignalsHeaderAdSlot to finish JSON parsing.
if (direct_from_seller_signals_header_ad_slot_pending_) {
++num_scoring_dependencies_;
}
DecodeAdditionalBidsIfReady();
// Need to start loading worklets before any bids can be generated or scored.
if (component_auctions_.empty()) {
// If there are no component auctions, request the seller worklet if we may
// need it. (The case for component auctions is handled below, there the
// seller worklet will be requested once all component auctions have
// received their own seller worklets).
if (!buyer_helpers_.empty() || MayHaveAdditionalBids()) {
RequestSellerWorklet();
}
} else {
// Since component auctions may invoke OnComponentSellerWorkletReceived()
// synchronously, it's important to set this to the total number of
// component auctions before invoking StartBiddingAndScoringPhase() on any
// component auction.
pending_component_seller_worklet_requests_ = component_auctions_.size();
for (auto& component_auction_info : component_auctions_) {
InterestGroupAuction* component_auction =
component_auction_info.second.get();
component_auction->StartBiddingAndScoringPhase(
base::BindOnce(
&InterestGroupAuction::OnComponentSellerWorkletReceived,
base::Unretained(this)),
base::BindOnce(&InterestGroupAuction::OnComponentAuctionComplete,
base::Unretained(this), component_auction));
}
}
for (const auto& buyer_helper : buyer_helpers_) {
buyer_helper->StartGeneratingBids();
}
// It's possible that we actually have nothing to do here at this point ---
// neither bids from interest groups, nor configuration promises to wait for.
MaybeCompleteBiddingAndScoringPhase();
}
void InterestGroupAuction::StartFromServerResponse(
mojo_base::BigBuffer response,
AdAuctionPageData* ad_auction_page_data,
AuctionPhaseCompletionCallback bidding_and_scoring_phase_callback) {
bidding_and_scoring_phase_callback_ =
std::move(bidding_and_scoring_phase_callback);
is_server_auction_ = true;
// Check that response was witnessed by seller origin.
std::array<uint8_t, crypto::kSHA256Length> hash =
crypto::SHA256Hash(response.byte_span());
if (!ad_auction_page_data->WitnessedAuctionResultForOrigin(
config_->seller,
std::string(reinterpret_cast<char*>(hash.data()), hash.size()))) {
// If it wasn't witnessed then we don't know that it came from the server.
OnBiddingAndScoringComplete(
AuctionResult::kInvalidServerResponse,
{base::StrCat(
{"runAdAuction(): Server response was not witnessed from ",
config_->seller.Serialize()})});
return;
}
AdAuctionRequestContext* request_context =
ad_auction_page_data->GetContextForAdAuctionRequest(
config_->server_response->request_id);
if (!request_context) {
// The corresponding context for the requested blob couldn't be found.
OnBiddingAndScoringComplete(
AuctionResult::kInvalidServerResponse,
{base::StrCat(
{"runAdAuction(): No corresponding request with ID: ",
config_->server_response->request_id.AsLowercaseString()})});
return;
}
// The auction must be for the same seller that requested the blob.
if (request_context->seller != config_->seller) {
OnBiddingAndScoringComplete(
AuctionResult::kInvalidServerResponse,
{"runAdAuction(): Seller in response doesn't match request"});
return;
}
get_ad_auction_data_start_time_ = request_context->start_time;
auto maybe_response =
quiche::ObliviousHttpResponse::CreateClientObliviousResponse(
std::string(reinterpret_cast<char*>(response.data()),
response.size()),
request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType.Get());
if (!maybe_response.ok()) {
// We couldn't decrypt the response.
OnBiddingAndScoringComplete(
AuctionResult::kInvalidServerResponse,
{"runAdAuction(): Could not decrypt server response"});
return;
}
const std::string& plaintext_response = maybe_response->GetPlaintextData();
absl::optional<base::span<const uint8_t>> compressed_response =
ExtractCompressedBiddingAndAuctionResponse(
base::as_bytes(base::make_span(plaintext_response)));
if (!compressed_response) {
OnBiddingAndScoringComplete(
AuctionResult::kInvalidServerResponse,
{"runAdAuction(): Could not parse response framing"});
return;
}
data_decoder_->GzipUncompress(
std::move(compressed_response).value(),
base::BindOnce(
&InterestGroupAuction::OnDecompressedServerResponse,
// This use of unretained is safe since the request_context is owned
// by a PageUserData and is only deleted when the page is destroyed.
// This auction is owned by the page through a DocumentService, so it
// will be destroyed before the decoder.
weak_ptr_factory_.GetWeakPtr(), base::Unretained(request_context)));
}
std::unique_ptr<InterestGroupAuctionReporter>
InterestGroupAuction::CreateReporter(
BrowserContext* browser_context,
PrivateAggregationManager* private_aggregation_manager,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
std::unique_ptr<blink::AuctionConfig> auction_config,
const url::Origin& main_frame_origin,
const url::Origin& frame_origin,
network::mojom::ClientSecurityStatePtr client_security_state,
blink::InterestGroupSet interest_groups_that_bid) {
DCHECK(!load_interest_groups_phase_callback_);
DCHECK(!bidding_and_scoring_phase_callback_);
DCHECK_EQ(*final_auction_result_, AuctionResult::kSuccess);
DCHECK(non_kanon_enforced_auction_leader_.top_bid);
// This should only be called on top-level auctions.
DCHECK(!parent_);
uint64_t trace_id = *trace_id_;
trace_id_.reset();
const LeaderInfo& leader = leader_info();
InterestGroupAuction::ScoredBid* winner = leader.top_bid.get();
InterestGroupAuctionReporter::WinningBidInfo winning_bid_info(
winner->bid->bid_state->bidder);
winning_bid_info.render_url = winner->bid->ad_descriptor.url;
winning_bid_info.ad_components = winner->bid->GetAdComponentUrls();
winning_bid_info.allowed_reporting_origins =
winner->bid->bid_ad->allowed_reporting_origins;
// Need the bid from the bidder itself. If the bid was from a component
// auction, then `top_bid_->bid` will be the bid from the component auction,
// which the component seller worklet may have modified, and thus the wrong
// bid. As a result, have to get the top bid from the component auction in
// that case. `top_bid_->bid->auction->top_bid()` is the same as `top_bid_` if
// the bid was from the top-level auction, and the original top bid from the
// component auction, otherwise, so will always be the bid returned by the
// winning bidder's generateBid() method.
const InterestGroupAuction::Bid* bidder_bid =
winner->bid->auction->top_bid()->bid.get();
winning_bid_info.bid = bidder_bid->bid;
// We redact the bid currency to just be the concrete currency from the
// config; passing in the currency from actual bid would permit leakage of
// information, if only via its absence.
winning_bid_info.bid_currency = PerBuyerCurrency(
bidder_bid->interest_group->owner, *bidder_bid->auction->config_);
winning_bid_info.provided_as_additional_bid =
bidder_bid->bid_state->additional_bid_buyer.has_value();
winning_bid_info.ad_cost = bidder_bid->ad_cost;
winning_bid_info.modeling_signals = bidder_bid->modeling_signals;
winning_bid_info.bid_duration = winner->bid->bid_duration;
winning_bid_info.bidding_signals_data_version =
winner->bid->bidding_signals_data_version;
base::Value::Dict ad_metadata;
ad_metadata.Set("renderURL", winner->bid->ad_descriptor.url.spec());
if (winner->bid->bid_ad->metadata) {
ad_metadata.Set("metadata", winner->bid->bid_ad->metadata.value());
}
if (winner->bid->bid_ad->ad_render_id) {
ad_metadata.Set("adRenderId", winner->bid->bid_ad->ad_render_id.value());
}
JSONStringValueSerializer serializer(&winning_bid_info.ad_metadata);
serializer.Serialize(base::Value(std::move(ad_metadata)));
InterestGroupAuctionReporter::SellerWinningBidInfo
top_level_seller_winning_bid_info;
top_level_seller_winning_bid_info.auction_config = config_;
DCHECK(subresource_url_builder_); // Must have been created by scoring.
CHECK(direct_from_seller_signals_header_ad_slot_); // Should never be null.
top_level_seller_winning_bid_info.subresource_url_builder =
std::move(subresource_url_builder_);
top_level_seller_winning_bid_info.direct_from_seller_signals_header_ad_slot =
std::move(direct_from_seller_signals_header_ad_slot_);
top_level_seller_winning_bid_info.bid = winner->bid->bid;
if (winner->bid->auction == this) {
// Bid came directly from bidder, not a component auction.
top_level_seller_winning_bid_info.bid_currency =
winning_bid_info.bid_currency;
} else {
// Bid comes from component auction, so we redact the config expectation of
// the bid of component auction in top-level auction.
InterestGroupAuction* component_auction = winner->bid->auction;
top_level_seller_winning_bid_info.bid_currency =
component_auction->config_->non_shared_params.seller_currency;
if (!top_level_seller_winning_bid_info.bid_currency) {
top_level_seller_winning_bid_info.bid_currency =
PerBuyerCurrency(component_auction->config_->seller, *config_);
}
}
top_level_seller_winning_bid_info.bid_in_seller_currency =
winner->bid_in_seller_currency.value_or(0.0);
top_level_seller_winning_bid_info.score = winner->score;
top_level_seller_winning_bid_info.highest_scoring_other_bid =
leader.highest_scoring_other_bid;
top_level_seller_winning_bid_info
.highest_scoring_other_bid_in_seller_currency =
leader.highest_scoring_other_bid_in_seller_currency;
top_level_seller_winning_bid_info.highest_scoring_other_bid_owner =
leader.highest_scoring_other_bid_owner;
top_level_seller_winning_bid_info.scoring_signals_data_version =
leader.top_bid->scoring_signals_data_version;
top_level_seller_winning_bid_info.trace_id = trace_id;
// Populate the SellerWinningBidInfo for the component auction that the
// winning bid came from, if any. This largely duplicates the above block.
//
// TODO(mmenke): Share code with the above block. This currently isn't
// possible because InterestGroupAuctionReporter depends on
// InterestGroupAuction, so it can return an auction completion status, so no
// InterestGroupAuction methods can take or return an
// InterestGroupAuctionReporter::SellerWinningBidInfo. Once that dependency is
// removed, it should be possible to make a helper method to construct both
// SellerWinningBidInfos.
absl::optional<InterestGroupAuctionReporter::SellerWinningBidInfo>
component_seller_winning_bid_info;
if (winner->bid->auction != this) {
InterestGroupAuction* component_auction = winner->bid->auction;
component_seller_winning_bid_info.emplace();
component_seller_winning_bid_info->auction_config =
component_auction->config_;
DCHECK(component_auction->subresource_url_builder_);
// Should never be null.
CHECK(component_auction->direct_from_seller_signals_header_ad_slot_);
component_seller_winning_bid_info->subresource_url_builder =
std::move(component_auction->subresource_url_builder_);
component_seller_winning_bid_info
->direct_from_seller_signals_header_ad_slot = std::move(
component_auction->direct_from_seller_signals_header_ad_slot_);
const LeaderInfo& component_leader = component_auction->leader_info();
component_seller_winning_bid_info->bid = component_leader.top_bid->bid->bid;
// The bidder in this auction was the actual bidder, so the currency comes
// from it, too.
component_seller_winning_bid_info->bid_currency =
winning_bid_info.bid_currency;
component_seller_winning_bid_info->bid_in_seller_currency =
component_leader.top_bid->bid_in_seller_currency.value_or(0.0);
component_seller_winning_bid_info->score = component_leader.top_bid->score;
component_seller_winning_bid_info->highest_scoring_other_bid =
component_leader.highest_scoring_other_bid;
component_seller_winning_bid_info
->highest_scoring_other_bid_in_seller_currency =
component_leader.highest_scoring_other_bid_in_seller_currency;
component_seller_winning_bid_info->highest_scoring_other_bid_owner =
component_leader.highest_scoring_other_bid_owner;
component_seller_winning_bid_info->scoring_signals_data_version =
component_leader.top_bid->scoring_signals_data_version;
component_seller_winning_bid_info->trace_id = *component_auction->trace_id_;
component_seller_winning_bid_info->component_auction_modified_bid_params =
component_leader.top_bid->component_auction_modified_bid_params
->Clone();
}
std::vector<GURL> debug_win_report_urls;
std::vector<GURL> debug_loss_report_urls;
TakeDebugReportUrlsAndFillInPrivateAggregationRequests(
debug_win_report_urls, debug_loss_report_urls);
bool bid_is_kanon;
switch (kanon_mode_) {
case auction_worklet::mojom::KAnonymityBidMode::kSimulate:
// Check if the winner was k-anon, since we use the non-kanonymous winner
// in simulate mode.
bid_is_kanon = NonKAnonWinnerIsKAnon();
break;
case auction_worklet::mojom::KAnonymityBidMode::kEnforce:
// We always have a winner when we get here.
bid_is_kanon = true;
break;
case auction_worklet::mojom::KAnonymityBidMode::kNone:
// Set to false due to enforcement is completely off.
bid_is_kanon = false;
break;
}
auto result = std::make_unique<InterestGroupAuctionReporter>(
interest_group_manager_, auction_worklet_manager_, browser_context,
private_aggregation_manager,
maybe_log_private_aggregation_web_features_callback_,
std::move(auction_config), main_frame_origin, frame_origin,
std::move(client_security_state), std::move(url_loader_factory),
kanon_mode_, bid_is_kanon, std::move(winning_bid_info),
std::move(top_level_seller_winning_bid_info),
std::move(component_seller_winning_bid_info),
std::move(interest_groups_that_bid), std::move(debug_win_report_urls),
std::move(debug_loss_report_urls), GetKAnonKeysToJoin(),
TakeReservedPrivateAggregationRequests(),
TakeNonReservedPrivateAggregationRequests());
// Avoid dangling pointers for things transferred to the reporter.
winner->bid->interest_group = nullptr;
winner->bid->bid_ad = nullptr;
config_ = nullptr;
for (const auto& kv : component_auctions_) {
kv.second->config_ = nullptr;
}
return result;
}
void InterestGroupAuction::NotifyConfigPromisesResolved() {
DCHECK(!config_promises_resolved_);
DCHECK_EQ(0, config_->NumPromises());
config_promises_resolved_ = true;
// If we haven't started the bidding and scoring phase, we will just handle
// this information at its start; setting `config_promises_resolved_` is
// enough both for us and the BuyerHelper. If we are after the phase, that
// means that promises resolved after we failed out of it, so we should ignore
// them.
if (bidding_and_scoring_phase_state_ != PhaseState::kDuring) {
return;
}
auction_metrics_recorder_->OnConfigPromisesResolved();
for (const auto& buyer_helper : buyer_helpers_) {
buyer_helper->NotifyConfigPromisesResolved();
}
base::TimeTicks now = base::TimeTicks::Now();
for (auto& unscored_bid : unscored_bids_) {
unscored_bid->wait_promises =
now - unscored_bid->trace_wait_seller_deps_start;
if (!component_auctions_.empty()) {
auction_metrics_recorder_
->RecordTopLevelBidQueuedWaitingForConfigPromises(
unscored_bid->wait_promises);
} else {
auction_metrics_recorder_->RecordBidQueuedWaitingForConfigPromises(
unscored_bid->wait_promises);
}
}
DecodeAdditionalBidsIfReady();
// Config resolution is done.
OnScoringDependencyDone();
ScoreQueuedBidsIfReady();
}
void InterestGroupAuction::NotifyComponentConfigPromisesResolved(uint32_t pos) {
DCHECK(!parent_); // Should not be called on a component.
auto it = component_auctions_.find(pos);
if (it == component_auctions_.end()) {
// It's OK if the component auction isn't found; that means it got dropped
// at database loading stage.
return;
}
it->second->NotifyConfigPromisesResolved();
}
void InterestGroupAuction::NotifyAdditionalBidsConfig(
AdAuctionPageData* auction_page_data) {
// An auction with additional bids can't have child auctions.
DCHECK_EQ(config_->non_shared_params.component_auctions.size(), 0u);
// Enforced by mojo traits and checks on ResolvedAdditionalBids().
CHECK(config_->non_shared_params.auction_nonce.has_value());
CHECK(config_->non_shared_params.interest_group_buyers.has_value());
if (!auction_page_data) {
return;
}
interest_group_buyers_ = base::flat_set<url::Origin>(
config_->non_shared_params.interest_group_buyers->begin(),
config_->non_shared_params.interest_group_buyers->end());
encoded_signed_additional_bids_ =
auction_page_data->TakeAuctionAdditionalBidsForOriginAndNonce(
config_->seller,
config_->non_shared_params.auction_nonce->AsLowercaseString());
}
void InterestGroupAuction::NotifyComponentAdditionalBidsConfig(
uint32_t pos,
AdAuctionPageData* auction_page_data) {
DCHECK(!parent_); // Should not be called on a component.
// And should have component auctions configured if we got thus far
// (though none may have ended up passing the permissions check).
DCHECK_GT(config_->non_shared_params.component_auctions.size(), 0u);
auto it = component_auctions_.find(pos);
if (it == component_auctions_.end()) {
// Empty component auctions shouldn't be dropped, but component
// auctions may still be dropped if they fail permissions checks.
return;
}
it->second->NotifyAdditionalBidsConfig(auction_page_data);
}
void InterestGroupAuction::NotifyDirectFromSellerSignalsHeaderAdSlotConfig(
AdAuctionPageData* auction_page_data,
const absl::optional<std::string>&
direct_from_seller_signals_header_ad_slot) {
CHECK(!direct_from_seller_signals_header_ad_slot_pending_);
if (!direct_from_seller_signals_header_ad_slot ||
bidding_and_scoring_phase_state_ == PhaseState::kAfter) {
return;
}
if (bidding_and_scoring_phase_state_ == PhaseState::kDuring) {
++num_scoring_dependencies_;
}
direct_from_seller_signals_header_ad_slot_pending_ = true;
HeaderDirectFromSellerSignals::ParseAndFind(
base::BindRepeating(&InterestGroupAuction::GetDataDecoder,
weak_ptr_factory_.GetWeakPtr()),
auction_page_data->GetAuctionSignalsForOrigin(config_->seller),
*direct_from_seller_signals_header_ad_slot,
base::BindOnce(
&InterestGroupAuction::OnDirectFromSellerSignalHeaderAdSlotResolved,
weak_ptr_factory_.GetWeakPtr()));
}
void InterestGroupAuction::
NotifyComponentDirectFromSellerSignalsHeaderAdSlotConfig(
uint32_t pos,
AdAuctionPageData* auction_page_data,
const absl::optional<std::string>&
direct_from_seller_signals_header_ad_slot) {
CHECK(!parent_); // Should not be called on a component.
auto it = component_auctions_.find(pos);
if (it == component_auctions_.end()) {
// Empty component auctions shouldn't be dropped, but component
// auctions may still be dropped if they fail permissions checks.
return;
}
it->second->NotifyDirectFromSellerSignalsHeaderAdSlotConfig(
auction_page_data, std::move(direct_from_seller_signals_header_ad_slot));
}
void InterestGroupAuction::ClosePipes() {
weak_ptr_factory_.InvalidateWeakPtrs();
score_ad_receivers_.Clear();
for (auto& buyer_helper : buyer_helpers_) {
buyer_helper->ClosePipes();
}
seller_worklet_handle_.reset();
// There shouldn't be any pipes here for additional bids (though reporter
// may create some).
for (const auto& bid_state : bid_states_for_additional_bids_) {
DCHECK(!bid_state->worklet_handle);
DCHECK(!bid_state->bid_finalizer);
}
// Close pipes for component auctions as well.
for (auto& component_auction_info : component_auctions_) {
component_auction_info.second->ClosePipes();
}
}
size_t InterestGroupAuction::NumPotentialBidders() const {
size_t num_interest_groups = 0;
for (const auto& buyer_helper : buyer_helpers_) {
num_interest_groups += buyer_helper->num_potential_bidders();
}
for (const auto& component_auction_info : component_auctions_) {
num_interest_groups += component_auction_info.second->NumPotentialBidders();
}
return num_interest_groups;
}
void InterestGroupAuction::GetInterestGroupsThatBidAndReportBidCounts(
blink::InterestGroupSet& interest_groups) const {
if (!all_bids_scored_) {
return;
}
if (saved_response_) {
interest_groups.insert(saved_response_->bidding_groups.begin(),
saved_response_->bidding_groups.end());
for (const auto& ig_bid : interest_groups) {
interest_group_manager_->NotifyInterestGroupAccessed(
InterestGroupManagerImpl::InterestGroupObserver::
InterestGroupObserver::kBid,
ig_bid.owner, ig_bid.name);
}
return;
}
for (auto& buyer_helper : buyer_helpers_) {
buyer_helper->GetInterestGroupsThatBidAndReportBidCounts(interest_groups);
}
// Notify devtools of additional bids. These don't go into `interest_groups`,
// that's only things in the database.
for (const auto& bid_state : bid_states_for_additional_bids_) {
CHECK(bid_state->made_bid);
interest_group_manager_->NotifyInterestGroupAccessed(
InterestGroupManagerImpl::InterestGroupObserver::InterestGroupObserver::
kAdditionalBid,
bid_state->bidder->interest_group.owner,
bid_state->bidder->interest_group.name);
}
// Retrieve data from component auctions as well.
for (const auto& component_auction_info : component_auctions_) {
component_auction_info.second->GetInterestGroupsThatBidAndReportBidCounts(
interest_groups);
}
}
absl::optional<blink::AdSize> InterestGroupAuction::RequestedAdSize() const {
return config_->non_shared_params.requested_size;
}
base::StringPiece GetRejectReasonString(
const auction_worklet::mojom::RejectReason reject_reason) {
base::StringPiece reject_reason_str;
switch (reject_reason) {
case auction_worklet::mojom::RejectReason::kNotAvailable:
reject_reason_str = "not-available";
break;
case auction_worklet::mojom::RejectReason::kInvalidBid:
reject_reason_str = "invalid-bid";
break;
case auction_worklet::mojom::RejectReason::kBidBelowAuctionFloor:
reject_reason_str = "bid-below-auction-floor";
break;
case auction_worklet::mojom::RejectReason::kPendingApprovalByExchange:
reject_reason_str = "pending-approval-by-exchange";
break;
case auction_worklet::mojom::RejectReason::kDisapprovedByExchange:
reject_reason_str = "disapproved-by-exchange";
break;
case auction_worklet::mojom::RejectReason::kBlockedByPublisher:
reject_reason_str = "blocked-by-publisher";
break;
case auction_worklet::mojom::RejectReason::kLanguageExclusions:
reject_reason_str = "language-exclusions";
break;
case auction_worklet::mojom::RejectReason::kCategoryExclusions:
reject_reason_str = "category-exclusions";
break;
case auction_worklet::mojom::RejectReason::kBelowKAnonThreshold:
reject_reason_str = "below-kanon-threshold";
break;
case auction_worklet::mojom::RejectReason::kWrongGenerateBidCurrency:
reject_reason_str = "wrong-generate-bid-currency";
break;
case auction_worklet::mojom::RejectReason::kWrongScoreAdCurrency:
reject_reason_str = "wrong-score-ad-currency";
break;
}
return reject_reason_str;
}
GURL InterestGroupAuction::FillPostAuctionSignals(
const GURL& url,
const PostAuctionSignals& signals,
const absl::optional<PostAuctionSignals>& top_level_signals,
const absl::optional<auction_worklet::mojom::RejectReason> reject_reason) {
// TODO(qingxinwu): Round `winning_bid` and `highest_scoring_other_bid` to two
// most-significant digits. Maybe same to corresponding browser signals of
// reportWin()/reportResult().
if (!url.has_query()) {
return url;
}
std::string query_string = url.query();
base::ReplaceSubstringsAfterOffset(&query_string, 0, "${winningBid}",
base::NumberToString(signals.winning_bid));
base::ReplaceSubstringsAfterOffset(
&query_string, 0, "${winningBidCurrency}",
blink::PrintableAdCurrency(signals.winning_bid_currency));
base::ReplaceSubstringsAfterOffset(
&query_string, 0, "${madeWinningBid}",
signals.made_winning_bid ? "true" : "false");
base::ReplaceSubstringsAfterOffset(
&query_string, 0, "${highestScoringOtherBid}",
base::NumberToString(signals.highest_scoring_other_bid));
base::ReplaceSubstringsAfterOffset(
&query_string, 0, "${highestScoringOtherBidCurrency}",
blink::PrintableAdCurrency(signals.highest_scoring_other_bid_currency));
base::ReplaceSubstringsAfterOffset(
&query_string, 0, "${madeHighestScoringOtherBid}",
signals.made_highest_scoring_other_bid ? "true" : "false");
// For component auction sellers only, which get post auction signals from
// both their own component auctions and top-level auction.
// For now, we're assuming top-level auctions to be first-price auction only
// (not second-price auction) and it does not need highest_scoring_other_bid.
if (top_level_signals.has_value()) {
base::ReplaceSubstringsAfterOffset(
&query_string, 0, "${topLevelWinningBid}",
base::NumberToString(top_level_signals->winning_bid));
base::ReplaceSubstringsAfterOffset(
&query_string, 0, "${topLevelWinningBidCurrency}",
blink::PrintableAdCurrency(top_level_signals->winning_bid_currency));
base::ReplaceSubstringsAfterOffset(
&query_string, 0, "${topLevelMadeWinningBid}",
top_level_signals->made_winning_bid ? "true" : "false");
}
if (reject_reason.has_value()) {
base::ReplaceSubstringsAfterOffset(
&query_string, 0, "${rejectReason}",
GetRejectReasonString(reject_reason.value()));
}
GURL::Replacements replacements;
replacements.SetQueryStr(query_string);
return url.ReplaceComponents(replacements);
}
bool InterestGroupAuction::ReportPaBuyersValueIfAllowed(
const blink::InterestGroup& interest_group,
blink::SellerCapabilities capability,
blink::AuctionConfig::NonSharedParams::BuyerReportType buyer_report_type,
int value) {
if (!CanReportPaBuyersValue(interest_group, capability, config_->seller)) {
return false;
}
absl::optional<absl::uint128> bucket_base =
BucketBaseForReportPaBuyers(*config_, interest_group.owner);
if (!bucket_base) {
return false;
}
absl::optional<
blink::AuctionConfig::NonSharedParams::AuctionReportBuyersConfig>
report_buyers_config =
ReportBuyersConfigForPaBuyers(buyer_report_type, *config_);
if (!report_buyers_config) {
return false;
}
// TODO(caraitto): Consider adding renderer and Mojo validation to ensure that
// bucket sums can't be out of range, and scales can't be negative, infinite,
// or NaN.
InterestGroupAuctionReporter::PrivateAggregationKey agg_key = {
config_->seller, config_->aggregation_coordinator_origin};
PrivateAggregationRequests& destination_vector =
private_aggregation_requests_reserved_[std::move(agg_key)];
destination_vector.push_back(
auction_worklet::mojom::PrivateAggregationRequest::New(
auction_worklet::mojom::AggregatableReportContribution::
NewHistogramContribution(
blink::mojom::AggregatableReportHistogramContribution::New(
*bucket_base + report_buyers_config->bucket,
base::saturated_cast<int32_t>(
std::max(0.0, value * report_buyers_config->scale)))),
// TODO(caraitto): Consider allowing these to be set.
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New()));
return true;
}
bool InterestGroupAuction::HasNonKAnonWinner() const {
if (!final_auction_result_) {
return false;
}
switch (*final_auction_result_) {
// Bidding and scoring phase completed with no fatal error. We either
// succeeded or only failed because we did not have a winner. If the only
// reason we didn't have a winner was k-anonymity enforcement, there may
// still be a non-k-anon winner.
case AuctionResult::kSuccess:
case AuctionResult::kNoBids:
case AuctionResult::kAllBidsRejected:
return top_non_kanon_enforced_bid() != nullptr;
case AuctionResult::kAborted:
case AuctionResult::kBadMojoMessage:
case AuctionResult::kNoInterestGroups:
case AuctionResult::kSellerWorkletLoadFailed:
case AuctionResult::kSellerWorkletCrashed:
case AuctionResult::kSellerRejected:
case AuctionResult::kComponentLostAuction:
case AuctionResult::kInvalidServerResponse:
case AuctionResult::kInvalidAuctionNonce:
return false;
}
}
bool InterestGroupAuction::NonKAnonWinnerIsKAnon() const {
return top_non_kanon_enforced_bid() &&
top_non_kanon_enforced_bid()
->bid->auction->top_non_kanon_enforced_bid()
->bid->bid_role == Bid::BidRole::kBothKAnonModes;
}
bool InterestGroupAuction::HasInterestGroups() const {
if (!buyer_helpers_.empty()) {
return true;
}
for (const auto& kv : component_auctions_) {
if (!kv.second->buyer_helpers_.empty()) {
return true;
}
}
return false;
}
SubresourceUrlBuilder* InterestGroupAuction::SubresourceUrlBuilderIfReady() {
if (!subresource_url_builder_ &&
!config_->direct_from_seller_signals.is_promise()) {
subresource_url_builder_ = std::make_unique<SubresourceUrlBuilder>(
config_->direct_from_seller_signals.value());
}
return subresource_url_builder_.get();
}
void InterestGroupAuction::
TakeDebugReportUrlsAndFillInPrivateAggregationRequests(
std::vector<GURL>& debug_win_report_urls,
std::vector<GURL>& debug_loss_report_urls) {
if (!all_bids_scored_) {
return;
}
// Set `winner` to the BidState in this auction associated with the winning
// bid of the top-level auction, if there is one.
//
// In a component auction, the highest bid may have lost the top-level
// auction, and we want to report that as a loss. In this case, AuctionResult
// will be kComponentLostAuction.
//
// Also for the top-level auction in the case a component auctions bid won,
// the highest bid's BidState and its reporting URLs are stored with the
// component auction, so the component auction will be the one to populate
// `debug_win_report_urls`.
BidState* winner = nullptr;
const LeaderInfo& leader = leader_info();
if (final_auction_result_ == AuctionResult::kSuccess &&
leader.top_bid->bid->auction == this) {
winner = leader.top_bid->bid->bid_state;
}
BidState* non_kanon_winner = nullptr;
if (kanon_mode_ == auction_worklet::mojom::KAnonymityBidMode::kEnforce &&
HasNonKAnonWinner() && !NonKAnonWinnerIsKAnon()) {
non_kanon_winner = top_non_kanon_enforced_bid()->bid->bid_state;
}
// `signals` includes post auction signals from current auction.
PostAuctionSignals signals;
// `top_level_signals` includes post auction signals from top-level auction.
// Will only will be used in debug report URLs of top-level seller and
// component sellers.
// For now, we're assuming top-level auctions to be first-price auction only
// (not second-price auction) and it does not need highest_scoring_other_bid.
absl::optional<PostAuctionSignals> top_level_signals;
if (parent_) {
top_level_signals.emplace();
}
if (!leader.top_bid) {
DCHECK_EQ(leader.highest_scoring_other_bid, 0);
DCHECK(!leader.highest_scoring_other_bid_owner.has_value());
}
std::map<InterestGroupAuctionReporter::PrivateAggregationKey,
PrivateAggregationRequests>
private_aggregation_requests_reserved;
std::map<std::string, PrivateAggregationRequests>
private_aggregation_requests_non_reserved;
for (const auto& buyer_helper : buyer_helpers_) {
const url::Origin& owner = buyer_helper->owner();
ComputePostAuctionSignals(owner, signals, top_level_signals);
buyer_helper->TakeDebugReportUrls(winner, signals, top_level_signals,
debug_win_report_urls,
debug_loss_report_urls);
buyer_helper->TakePrivateAggregationRequests(
winner, non_kanon_winner, signals, top_level_signals,
private_aggregation_requests_reserved,
private_aggregation_requests_non_reserved);
}
for (std::unique_ptr<BidState>& bid_state : bid_states_for_additional_bids_) {
const url::Origin& owner = bid_state->additional_bid_buyer.value();
ComputePostAuctionSignals(owner, signals, top_level_signals);
TakePrivateAggregationRequestsForBidState(
bid_state, /*is_component_auction=*/parent_ != nullptr, winner,
non_kanon_winner, signals, top_level_signals,
private_aggregation_requests_reserved,
private_aggregation_requests_non_reserved);
TakeDebugReportUrlsForBidState(bid_state, winner, signals,
top_level_signals, debug_win_report_urls,
debug_loss_report_urls);
}
for (auto& [key, requests] : private_aggregation_requests_reserved) {
PrivateAggregationRequests& destination_vector =
private_aggregation_requests_reserved_[key];
destination_vector.insert(destination_vector.end(),
std::move_iterator(requests.begin()),
std::move_iterator(requests.end()));
}
for (auto& [event_type, requests] :
private_aggregation_requests_non_reserved) {
PrivateAggregationRequests& destination_vector =
private_aggregation_requests_non_reserved_[event_type];
destination_vector.insert(destination_vector.end(),
std::move_iterator(requests.begin()),
std::move_iterator(requests.end()));
}
// Retrieve data from component auctions as well.
for (auto& component_auction_info : component_auctions_) {
component_auction_info.second
->TakeDebugReportUrlsAndFillInPrivateAggregationRequests(
debug_win_report_urls, debug_loss_report_urls);
}
}
std::map<InterestGroupAuctionReporter::PrivateAggregationKey,
InterestGroupAuction::PrivateAggregationRequests>
InterestGroupAuction::TakeReservedPrivateAggregationRequests() {
for (auto& component_auction_info : component_auctions_) {
std::map<InterestGroupAuctionReporter::PrivateAggregationKey,
PrivateAggregationRequests>
requests_map = component_auction_info.second
->TakeReservedPrivateAggregationRequests();
for (auto& [agg_key, requests] : requests_map) {
DCHECK(!requests.empty());
PrivateAggregationRequests& destination_vector =
private_aggregation_requests_reserved_[agg_key];
destination_vector.insert(destination_vector.end(),
std::move_iterator(requests.begin()),
std::move_iterator(requests.end()));
}
}
return std::move(private_aggregation_requests_reserved_);
}
std::map<std::string, InterestGroupAuction::PrivateAggregationRequests>
InterestGroupAuction::TakeNonReservedPrivateAggregationRequests() {
for (auto& component_auction_info : component_auctions_) {
std::map<std::string, PrivateAggregationRequests> requests_map =
component_auction_info.second
->TakeNonReservedPrivateAggregationRequests();
for (auto& [event_type, requests] : requests_map) {
DCHECK(!requests.empty());
PrivateAggregationRequests& destination_vector =
private_aggregation_requests_non_reserved_[event_type];
destination_vector.insert(destination_vector.end(),
std::move_iterator(requests.begin()),
std::move_iterator(requests.end()));
}
}
return std::move(private_aggregation_requests_non_reserved_);
}
std::vector<std::string> InterestGroupAuction::TakeErrors() {
for (auto& component_auction_info : component_auctions_) {
std::vector<std::string> errors =
component_auction_info.second->TakeErrors();
errors_.insert(errors_.begin(), errors.begin(), errors.end());
}
return std::move(errors_);
}
void InterestGroupAuction::TakePostAuctionUpdateOwners(
std::vector<url::Origin>& owners) {
for (const url::Origin& owner : post_auction_update_owners_) {
owners.emplace_back(std::move(owner));
}
for (auto& component_auction_info : component_auctions_) {
component_auction_info.second->TakePostAuctionUpdateOwners(owners);
}
}
bool InterestGroupAuction::ReportInterestGroupCount(
const blink::InterestGroup& interest_group,
size_t count) {
return ReportPaBuyersValueIfAllowed(
interest_group, blink::SellerCapabilities::kInterestGroupCounts,
blink::AuctionConfig::NonSharedParams::BuyerReportType::
kInterestGroupCount,
count);
}
bool InterestGroupAuction::ReportBidCount(
const blink::InterestGroup& interest_group,
size_t count) {
return ReportPaBuyersValueIfAllowed(
interest_group, blink::SellerCapabilities::kInterestGroupCounts,
blink::AuctionConfig::NonSharedParams::BuyerReportType::kBidCount, count);
}
void InterestGroupAuction::ReportTrustedSignalsFetchLatency(
const blink::InterestGroup& interest_group,
base::TimeDelta trusted_signals_fetch_latency) {
ReportPaBuyersValueIfAllowed(interest_group,
blink::SellerCapabilities::kLatencyStats,
blink::AuctionConfig::NonSharedParams::
BuyerReportType::kTotalSignalsFetchLatency,
trusted_signals_fetch_latency.InMilliseconds());
}
void InterestGroupAuction::ReportBiddingLatency(
const blink::InterestGroup& interest_group,
base::TimeDelta bidding_latency) {
ReportPaBuyersValueIfAllowed(interest_group,
blink::SellerCapabilities::kLatencyStats,
blink::AuctionConfig::NonSharedParams::
BuyerReportType::kTotalGenerateBidLatency,
bidding_latency.InMilliseconds());
}
base::flat_set<std::string> InterestGroupAuction::GetKAnonKeysToJoin() const {
if (!HasNonKAnonWinner()) {
return {};
}
// If the k-anon winner is the same as the non-k-anon winner then just include
// it once.
std::vector<const ScoredBid*> bids_to_include = {
top_non_kanon_enforced_bid()};
if (!NonKAnonWinnerIsKAnon()) {
bids_to_include.push_back(top_kanon_enforced_bid());
}
// Add all the KAnon keys for the winning k-anon and non-k-anon bids.
std::vector<std::string> k_anon_keys_to_join;
for (const ScoredBid* scored_bid : bids_to_include) {
if (!scored_bid) {
continue;
}
if (scored_bid->bid->bid_state->additional_bid_buyer.has_value()) {
continue;
}
DCHECK(scored_bid->bid);
const blink::InterestGroup& interest_group =
*scored_bid->bid->interest_group;
k_anon_keys_to_join.push_back(blink::KAnonKeyForAdBid(
interest_group, scored_bid->bid->bid_ad->render_url));
k_anon_keys_to_join.push_back(blink::KAnonKeyForAdNameReporting(
interest_group, *scored_bid->bid->bid_ad));
for (const blink::AdDescriptor& ad_component_descriptor :
scored_bid->bid->ad_component_descriptors) {
k_anon_keys_to_join.push_back(
blink::KAnonKeyForAdComponentBid(ad_component_descriptor));
}
}
return base::flat_set<std::string>(std::move(k_anon_keys_to_join));
}
void InterestGroupAuction::MaybeLogPrivateAggregationWebFeatures(
const std::vector<auction_worklet::mojom::PrivateAggregationRequestPtr>&
private_aggregation_requests) {
DCHECK(maybe_log_private_aggregation_web_features_callback_);
maybe_log_private_aggregation_web_features_callback_.Run(
private_aggregation_requests);
}
const InterestGroupAuction::LeaderInfo& InterestGroupAuction::leader_info()
const {
if (kanon_mode_ == auction_worklet::mojom::KAnonymityBidMode::kEnforce) {
return kanon_enforced_auction_leader_;
} else {
return non_kanon_enforced_auction_leader_;
}
}
InterestGroupAuction::ScoredBid*
InterestGroupAuction::top_kanon_enforced_bid() {
return kanon_enforced_auction_leader_.top_bid.get();
}
const InterestGroupAuction::ScoredBid*
InterestGroupAuction::top_kanon_enforced_bid() const {
return kanon_enforced_auction_leader_.top_bid.get();
}
InterestGroupAuction::ScoredBid*
InterestGroupAuction::top_non_kanon_enforced_bid() {
return non_kanon_enforced_auction_leader_.top_bid.get();
}
const InterestGroupAuction::ScoredBid*
InterestGroupAuction::top_non_kanon_enforced_bid() const {
return non_kanon_enforced_auction_leader_.top_bid.get();
}
void InterestGroupAuction::ComputePostAuctionSignals(
const url::Origin& bid_owner,
PostAuctionSignals& signals_out,
absl::optional<PostAuctionSignals>& top_level_signals_out) {
DCHECK(!parent_ || top_level_signals_out.has_value());
const LeaderInfo& leader = leader_info();
if (leader.top_bid) {
PostAuctionSignals::FillWinningBidInfo(
bid_owner, leader.top_bid->bid->interest_group->owner,
leader.top_bid->bid->bid, leader.top_bid->bid_in_seller_currency,
config_->non_shared_params.seller_currency,
signals_out.made_winning_bid, signals_out.winning_bid,
signals_out.winning_bid_currency);
}
PostAuctionSignals::FillRelevantHighestScoringOtherBidInfo(
bid_owner, leader.highest_scoring_other_bid_owner,
leader.highest_scoring_other_bid,
leader.highest_scoring_other_bid_in_seller_currency,
config_->non_shared_params.seller_currency,
signals_out.made_highest_scoring_other_bid,
signals_out.highest_scoring_other_bid,
signals_out.highest_scoring_other_bid_currency);
if (parent_ && parent_->top_bid()) {
PostAuctionSignals::FillWinningBidInfo(
bid_owner, parent_->top_bid()->bid->interest_group->owner,
parent_->top_bid()->bid->bid,
parent_->top_bid()->bid_in_seller_currency,
parent_->config_->non_shared_params.seller_currency,
top_level_signals_out->made_winning_bid,
top_level_signals_out->winning_bid,
top_level_signals_out->winning_bid_currency);
}
}
absl::optional<uint16_t> InterestGroupAuction::GetBuyerExperimentId(
const blink::AuctionConfig& config,
const url::Origin& buyer) {
auto it = config.per_buyer_experiment_group_ids.find(buyer);
if (it != config.per_buyer_experiment_group_ids.end()) {
return it->second;
}
return config.all_buyer_experiment_group_id;
}
absl::optional<std::string> InterestGroupAuction::GetPerBuyerSignals(
const blink::AuctionConfig& config,
const url::Origin& buyer) {
const auto& auction_config_per_buyer_signals =
config.non_shared_params.per_buyer_signals;
DCHECK(!auction_config_per_buyer_signals.is_promise());
if (auction_config_per_buyer_signals.value().has_value()) {
auto it = auction_config_per_buyer_signals.value()->find(buyer);
if (it != auction_config_per_buyer_signals.value()->end()) {
return it->second;
}
}
return absl::nullopt;
}
absl::optional<GURL> InterestGroupAuction::GetDirectFromSellerAuctionSignals(
const SubresourceUrlBuilder* subresource_url_builder) {
if (subresource_url_builder && subresource_url_builder->auction_signals()) {
return subresource_url_builder->auction_signals()->subresource_url;
}
return absl::nullopt;
}
absl::optional<std::string>
InterestGroupAuction::GetDirectFromSellerAuctionSignalsHeaderAdSlot(
const HeaderDirectFromSellerSignals& signals) {
return signals.auction_signals();
}
absl::optional<GURL> InterestGroupAuction::GetDirectFromSellerPerBuyerSignals(
const SubresourceUrlBuilder* subresource_url_builder,
const url::Origin& owner) {
if (!subresource_url_builder) {
return absl::nullopt;
}
auto it = subresource_url_builder->per_buyer_signals().find(owner);
if (it == subresource_url_builder->per_buyer_signals().end()) {
return absl::nullopt;
}
return it->second.subresource_url;
}
absl::optional<std::string>
InterestGroupAuction::GetDirectFromSellerPerBuyerSignalsHeaderAdSlot(
const HeaderDirectFromSellerSignals& signals,
const url::Origin& owner) {
auto it = signals.per_buyer_signals().find(owner);
if (it == signals.per_buyer_signals().end()) {
return absl::nullopt;
}
return it->second;
}
absl::optional<GURL> InterestGroupAuction::GetDirectFromSellerSellerSignals(
const SubresourceUrlBuilder* subresource_url_builder) {
if (subresource_url_builder && subresource_url_builder->seller_signals()) {
return subresource_url_builder->seller_signals()->subresource_url;
}
return absl::nullopt;
}
absl::optional<std::string>
InterestGroupAuction::GetDirectFromSellerSellerSignalsHeaderAdSlot(
const HeaderDirectFromSellerSignals& signals) {
return signals.seller_signals();
}
InterestGroupAuction::LeaderInfo::LeaderInfo() = default;
InterestGroupAuction::LeaderInfo::~LeaderInfo() = default;
void InterestGroupAuction::OnInterestGroupRead(
scoped_refptr<StorageInterestGroups> read_interest_groups) {
++num_owners_loaded_;
if (read_interest_groups->size() == 0) {
OnOneLoadCompleted();
return;
}
std::vector<SingleStorageInterestGroup> interest_groups =
read_interest_groups->GetInterestGroups();
for (const SingleStorageInterestGroup& group : interest_groups) {
if (ReportInterestGroupCount(group->interest_group,
read_interest_groups->size())) {
break;
}
}
post_auction_update_owners_.emplace_back(
interest_groups[0]->interest_group.owner);
for (const SingleStorageInterestGroup& bidder : interest_groups) {
// Report freshness metrics.
if (bidder->interest_group.update_url.has_value()) {
UMA_HISTOGRAM_CUSTOM_COUNTS(
"Ads.InterestGroup.Auction.GroupFreshness.WithDailyUpdates",
(base::Time::Now() - bidder->last_updated).InMinutes(),
kGroupFreshnessMin.InMinutes(), kGroupFreshnessMax.InMinutes(),
kGroupFreshnessBuckets);
} else {
UMA_HISTOGRAM_CUSTOM_COUNTS(
"Ads.InterestGroup.Auction.GroupFreshness.NoDailyUpdates",
(base::Time::Now() - bidder->last_updated).InMinutes(),
kGroupFreshnessMin.InMinutes(), kGroupFreshnessMax.InMinutes(),
kGroupFreshnessBuckets);
}
}
// Extract negative targeting info, if something may care.
// This needs to happen before dropping IGs w/o scripts and ads, since
// negative targeting IGs may lack those.
if (MayHaveAdditionalBids()) {
for (const SingleStorageInterestGroup& group : interest_groups) {
if (group->interest_group.additional_bid_key.has_value()) {
negative_targeter_->AddInterestGroupInfo(
group->interest_group.owner, group->interest_group.name,
group->joining_origin, *group->interest_group.additional_bid_key);
}
}
}
// Ignore interest groups with no bidding script or no ads.
base::EraseIf(interest_groups, [](const SingleStorageInterestGroup& bidder) {
return !bidder->interest_group.bidding_url || !bidder->interest_group.ads ||
bidder->interest_group.ads->empty();
});
// Ignore interest groups that don't provide the requested seller
// capabilities.
base::EraseIf(interest_groups,
[this](const SingleStorageInterestGroup& bidder) {
return !GroupSatisfiesAllCapabilities(
bidder->interest_group,
config_->non_shared_params.required_seller_capabilities,
config_->seller);
});
// If there are no interest groups left, nothing else to do.
if (interest_groups.empty()) {
OnOneLoadCompleted();
return;
}
++num_owners_with_interest_groups_;
auction_metrics_recorder_->ReportBuyer(
interest_groups[0]->interest_group.owner);
auto buyer_helper =
std::make_unique<BuyerHelper>(this, std::move(interest_groups));
// BuyerHelper may filter out additional interest groups on construction.
if (buyer_helper->has_potential_bidder()) {
buyer_helpers_.emplace_back(std::move(buyer_helper));
} else {
// `buyer_helper` has a raw pointer to `this`, so if it's not added to
// buyer_helpers_, delete it now to avoid a dangling pointer, since
// OnOneLoadCompleted() could result in deleting `this`.
buyer_helper.reset();
}
OnOneLoadCompleted();
}
void InterestGroupAuction::OnComponentInterestGroupsRead(
AuctionMap::iterator component_auction,
bool success) {
num_owners_loaded_ += component_auction->second->num_owners_loaded_;
num_owners_with_interest_groups_ +=
component_auction->second->num_owners_with_interest_groups_;
// Erase component auctions that failed their interest group loading phase,
// so they won't be invoked in the generate bid phase. This is not a problem
// in the reporting phase, as the top-level auction knows which component
// auction, if any, won.
if (!success) {
component_auctions_.erase(component_auction);
}
OnOneLoadCompleted();
}
void InterestGroupAuction::OnOneLoadCompleted() {
DCHECK_GT(num_pending_loads_, 0u);
--num_pending_loads_;
// Wait for more buyers to be loaded, if there are still some pending.
if (num_pending_loads_ > 0) {
return;
}
// Record histograms about the interest groups participating in the auction.
// TODO(mmenke): Record histograms for component auctions.
if (!parent_) {
// Only record histograms if there were interest groups that could
// theoretically participate in the auction.
if (num_owners_loaded_ > 0) {
size_t num_interest_groups = NumPotentialBidders();
size_t num_sellers_with_bidders = 0;
for (const auto& [unused, component] : component_auctions_) {
if (!component->buyer_helpers_.empty()) {
++num_sellers_with_bidders;
}
}
// If the top-level seller either has interest groups itself, or any of
// the component auctions do, then the top-level seller also has bidders.
if (num_interest_groups > 0) {
++num_sellers_with_bidders;
}
auction_metrics_recorder_->SetNumInterestGroups(num_interest_groups);
auction_metrics_recorder_->SetNumOwnersWithInterestGroups(
num_owners_with_interest_groups_);
auction_metrics_recorder_->SetNumSellersWithBidders(
num_sellers_with_bidders);
}
}
if (MayHaveAdditionalBids()) {
auction_metrics_recorder_->RecordNegativeInterestGroups(
negative_targeter_->GetNumNegativeInterestGroups());
}
// We generally proceed even if there is seemingly nothing to do since we
// still may need to wait for promises to resolve and deal with
// `additional_bids`.
AuctionResult result = AuctionResult::kSuccess;
// If the config had components, but none survived the loading phase,
// they must all have failed permissions checks, so there is no reason
// to proceed.
if (!config_->non_shared_params.component_auctions.empty() &&
component_auctions_.empty()) {
result = AuctionResult::kNoInterestGroups;
}
OnStartLoadInterestGroupsPhaseComplete(result);
}
void InterestGroupAuction::OnStartLoadInterestGroupsPhaseComplete(
AuctionResult auction_result) {
DCHECK(load_interest_groups_phase_callback_);
DCHECK(!final_auction_result_);
if (!parent_) {
auction_metrics_recorder_->OnLoadInterestGroupPhaseComplete();
}
TRACE_EVENT_NESTABLE_ASYNC_END0("fledge", "load_groups_phase", *trace_id_);
if (!HasInterestGroups()) {
UMA_HISTOGRAM_TIMES("Ads.InterestGroup.Auction.LoadNoGroupsTime",
base::TimeTicks::Now() - creation_time_);
} else {
UMA_HISTOGRAM_TIMES("Ads.InterestGroup.Auction.LoadGroupsTime",
base::TimeTicks::Now() - creation_time_);
}
// `final_auction_result_` should only be set to kSuccess when the entire
// auction is complete.
//
// TODO(https://crbug.com/1394777): We should probably add new states for
// whether the result was used, reports sent, etc, so either the
// InterestGroupAuction or the InterestGroupAuctionReporter logs a single
// result. Alternatively, we could add a separate histogram just for the
// reporter stuff, which should have exactly as many entries as the historam
// `final_auction_result_` is logged to.
bool success = auction_result == AuctionResult::kSuccess;
if (!success) {
final_auction_result_ = auction_result;
}
std::move(load_interest_groups_phase_callback_).Run(success);
}
void InterestGroupAuction::OnComponentSellerWorkletReceived() {
DCHECK_GT(pending_component_seller_worklet_requests_, 0u);
--pending_component_seller_worklet_requests_;
if (pending_component_seller_worklet_requests_ == 0) {
RequestSellerWorklet();
}
}
void InterestGroupAuction::RequestSellerWorklet() {
DCHECK_EQ(bidding_and_scoring_phase_state_, PhaseState::kDuring);
TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("fledge", "request_seller_worklet",
*trace_id_);
auction_worklet_manager_->RequestSellerWorklet(
*config_->decision_logic_url, config_->trusted_scoring_signals_url,
config_->seller_experiment_group_id,
base::BindOnce(&InterestGroupAuction::OnSellerWorkletReceived,
base::Unretained(this)),
base::BindOnce(&InterestGroupAuction::OnSellerWorkletFatalError,
base::Unretained(this)),
seller_worklet_handle_);
}
void InterestGroupAuction::OnSellerWorkletReceived() {
DCHECK(!seller_worklet_received_);
DCHECK_EQ(bidding_and_scoring_phase_state_, PhaseState::kDuring);
TRACE_EVENT_NESTABLE_ASYNC_END0("fledge", "request_seller_worklet",
*trace_id_);
if (on_seller_receiver_callback_) {
std::move(on_seller_receiver_callback_).Run();
}
seller_worklet_received_ = true;
base::TimeTicks now = base::TimeTicks::Now();
for (auto& unscored_bid : unscored_bids_) {
unscored_bid->wait_worklet =
now - unscored_bid->trace_wait_seller_deps_start;
if (!component_auctions_.empty()) {
auction_metrics_recorder_->RecordTopLevelBidQueuedWaitingForSellerWorklet(
unscored_bid->wait_worklet);
} else {
auction_metrics_recorder_->RecordBidQueuedWaitingForSellerWorklet(
unscored_bid->wait_worklet);
}
}
ScoreQueuedBidsIfReady();
}
void InterestGroupAuction::ScoreQueuedBidsIfReady() {
DCHECK_EQ(bidding_and_scoring_phase_state_, PhaseState::kDuring);
if (!ReadyToScoreBids() || unscored_bids_.empty()) {
return;
}
auto unscored_bids = std::move(unscored_bids_);
for (auto& unscored_bid : unscored_bids) {
TRACE_EVENT_NESTABLE_ASYNC_END1(
"fledge", "wait_for_seller_deps", unscored_bid->TraceId(), "data",
[&](perfetto::TracedValue trace_context) {
auto dict = std::move(trace_context).WriteDictionary();
if (!unscored_bid->wait_worklet.is_zero()) {
dict.Add("wait_worklet_ms",
unscored_bid->wait_worklet.InMillisecondsF());
}
if (!unscored_bid->wait_promises.is_zero()) {
dict.Add("wait_promises_ms",
unscored_bid->wait_promises.InMillisecondsF());
}
});
ScoreBidIfReady(std::move(unscored_bid));
}
// If no further bids are outstanding, now is the time to send a coalesced
// request for all the trusted seller signals (if some still are pending,
// OnScoringDependencyDone() will take care of it).
if (num_scoring_dependencies_ == 0) {
seller_worklet_handle_->GetSellerWorklet()->SendPendingSignalsRequests();
}
// No more unscored bids should be added, once the seller worklet has been
// received.
DCHECK(unscored_bids_.empty());
}
void InterestGroupAuction::HandleAdditionalBidError(AdditionalBidResult result,
std::string error) {
auction_metrics_recorder_->RecordAdditionalBidResult(result);
errors_.push_back(std::move(error));
OnScoringDependencyDone();
}
void InterestGroupAuction::DecodeAdditionalBidsIfReady() {
DCHECK_EQ(bidding_and_scoring_phase_state_, PhaseState::kDuring);
if (encoded_signed_additional_bids_.empty()) {
return;
}
decode_additional_bids_start_time_ = base::TimeTicks::Now();
num_scoring_dependencies_ += encoded_signed_additional_bids_.size();
for (const auto& encoded_signed_bid : encoded_signed_additional_bids_) {
std::string signed_additional_bid_data;
if (!base::Base64Decode(encoded_signed_bid, &signed_additional_bid_data,
base::Base64DecodePolicy::kForgiving)) {
HandleAdditionalBidError(
AdditionalBidResult::kRejectedDueToInvalidBase64,
"Unable to base64-decode a signed additional bid.");
continue;
}
data_decoder_->ParseJson(
signed_additional_bid_data,
base::BindOnce(&InterestGroupAuction::HandleDecodedSignedAdditionalBid,
weak_ptr_factory_.GetWeakPtr()));
}
encoded_signed_additional_bids_.clear();
}
void InterestGroupAuction::HandleDecodedSignedAdditionalBid(
data_decoder::DataDecoder::ValueOrError result) {
DCHECK_EQ(bidding_and_scoring_phase_state_, PhaseState::kDuring);
if (!result.has_value()) {
HandleAdditionalBidError(
AdditionalBidResult::kRejectedDueToSignedBidJsonParseError,
"Unable to parse signed additional bid as JSON: " + result.error());
return;
}
auto maybe_signed_additional_bid =
DecodeSignedAdditionalBid(std::move(result).value());
if (!maybe_signed_additional_bid.has_value()) {
HandleAdditionalBidError(
AdditionalBidResult::kRejectedDueToSignedBidDecodeError,
"Unable to decode signed additional bid: " +
maybe_signed_additional_bid.error());
return;
}
auto valid_signatures = maybe_signed_additional_bid->VerifySignatures();
data_decoder_->ParseJson(
maybe_signed_additional_bid->additional_bid_json,
base::BindOnce(&InterestGroupAuction::HandleDecodedAdditionalBid,
weak_ptr_factory_.GetWeakPtr(),
std::move(maybe_signed_additional_bid->signatures),
std::move(valid_signatures)));
}
void InterestGroupAuction::HandleDecodedAdditionalBid(
const std::vector<SignedAdditionalBidSignature>& signatures,
const std::vector<size_t>& valid_signatures,
data_decoder::DataDecoder::ValueOrError result) {
DCHECK_EQ(bidding_and_scoring_phase_state_, PhaseState::kDuring);
if (!result.has_value()) {
HandleAdditionalBidError(
AdditionalBidResult::kRejectedDueToJsonParseError,
"Unable to parse additional bid as JSON: " + result.error());
return;
}
base::expected<AdditionalBidDecodeResult, std::string> maybe_bid =
DecodeAdditionalBid(
this, result.value(),
config_->non_shared_params.auction_nonce.value(),
interest_group_buyers_, config_->seller,
parent_
? base::optional_ref<const url::Origin>(parent_->config_->seller)
: base::optional_ref<const url::Origin>(absl::nullopt));
if (!maybe_bid.has_value()) {
HandleAdditionalBidError(AdditionalBidResult::kRejectedDueToDecodeError,
std::move(maybe_bid).error());
return;
}
if (!is_interest_group_api_allowed_callback_.Run(
ContentBrowserClient::InterestGroupApiOperation::kBuy,
maybe_bid->bid_state->bidder->interest_group.owner)) {
HandleAdditionalBidError(
AdditionalBidResult::kRejectedDueToBuyerNotAllowed,
"Rejecting an additionalBid due to operation not allowed for buyer.");
return;
}
if (!blink::VerifyAdCurrencyCode(
PerBuyerCurrency(*maybe_bid->bid_state->additional_bid_buyer,
*config_),
maybe_bid->bid->bid_currency)) {
HandleAdditionalBidError(
AdditionalBidResult::kRejectedDueToCurrencyMismatch,
"Rejecting an additionalBid due to currency mismatch.");
return;
}
if (negative_targeter_->ShouldDropDueToNegativeTargeting(
*maybe_bid->bid_state->additional_bid_buyer,
maybe_bid->negative_target_joining_origin,
maybe_bid->negative_target_interest_group_names, signatures,
valid_signatures, config_->seller, *auction_metrics_recorder_,
errors_)) {
// We do *not* call HandleAdditionalBidError at this point because an
// additional bid being negative targeted is not an error scenario, so we
// don't want to record anything to the errors_.
auction_metrics_recorder_->RecordAdditionalBidResult(
AdditionalBidResult::kNegativeTargeted);
auction_metrics_recorder_->RecordAdditionalBidDecodeLatency(
base::TimeTicks::Now() - decode_additional_bids_start_time_);
OnScoringDependencyDone();
return;
}
auction_metrics_recorder_->RecordAdditionalBidResult(
AdditionalBidResult::kSentForScoring);
auction_metrics_recorder_->RecordAdditionalBidDecodeLatency(
base::TimeTicks::Now() - decode_additional_bids_start_time_);
bid_states_for_additional_bids_.push_back(std::move(maybe_bid->bid_state));
ScoreBidIfReady(std::move(maybe_bid->bid));
OnScoringDependencyDone();
}
void InterestGroupAuction::OnSellerWorkletFatalError(
AuctionWorkletManager::FatalErrorType fatal_error_type,
const std::vector<std::string>& errors) {
DCHECK_EQ(bidding_and_scoring_phase_state_, PhaseState::kDuring);
AuctionResult result;
switch (fatal_error_type) {
case AuctionWorkletManager::FatalErrorType::kScriptLoadFailed:
result = AuctionResult::kSellerWorkletLoadFailed;
break;
case AuctionWorkletManager::FatalErrorType::kWorkletCrash:
result = AuctionResult::kSellerWorkletCrashed;
break;
}
OnBiddingAndScoringComplete(result, errors);
}
void InterestGroupAuction::OnComponentAuctionComplete(
InterestGroupAuction* component_auction,
bool success) {
DCHECK_EQ(bidding_and_scoring_phase_state_, PhaseState::kDuring);
if (!component_auction->buyer_helpers_.empty() ||
!component_auction->bid_states_for_additional_bids_.empty()) {
auction_metrics_recorder_->RecordComponentAuctionLatency(
base::TimeTicks::Now() - bidding_and_scoring_phase_start_time_);
}
// TODO(morlovich): Can try to consolidate these as kBothKAnonModes when
// possible.
ScoredBid* non_kanon_enforced_bid =
component_auction->top_non_kanon_enforced_bid();
if (non_kanon_enforced_bid) {
// There is no need to potentially turn this into an k-anon enforced bid
// since that already happened when running the component auction.
ScoreBidIfReady(CreateBidFromComponentAuctionWinner(
non_kanon_enforced_bid, Bid::BidRole::kUnenforcedKAnon));
}
ScoredBid* kanon_bid = component_auction->top_kanon_enforced_bid();
if (kanon_bid) {
ScoreBidIfReady(CreateBidFromComponentAuctionWinner(
kanon_bid, Bid::BidRole::kEnforcedKAnon));
}
OnScoringDependencyDone();
}
// static
std::unique_ptr<InterestGroupAuction::Bid>
InterestGroupAuction::CreateBidFromComponentAuctionWinner(
const ScoredBid* scored_bid,
Bid::BidRole bid_role) {
// Create a copy of component Auction's bid, replacing values as necessary.
const Bid* component_bid = scored_bid->bid.get();
const auto* modified_bid_params =
scored_bid->component_auction_modified_bid_params.get();
DCHECK(modified_bid_params);
// Create a new event for the bid, since the component auction's event for
// it ended after the component auction scored the bid.
if (bid_role == Bid::BidRole::kEnforcedKAnon) {
if (!component_bid->bid_state->trace_id_for_kanon_scoring.has_value()) {
component_bid->bid_state->BeginTracingKAnonScoring();
}
} else {
if (!component_bid->bid_state->trace_id.has_value()) {
component_bid->bid_state->BeginTracing();
}
}
return std::make_unique<Bid>(
bid_role, modified_bid_params->ad,
modified_bid_params->has_bid ? modified_bid_params->bid
: component_bid->bid,
modified_bid_params->has_bid ? modified_bid_params->bid_currency
: component_bid->bid_currency,
component_bid->ad_cost, component_bid->ad_descriptor,
component_bid->ad_component_descriptors, component_bid->modeling_signals,
component_bid->bid_duration, component_bid->bidding_signals_data_version,
component_bid->bid_ad, component_bid->bid_state, component_bid->auction);
}
void InterestGroupAuction::OnScoringDependencyDone() {
DCHECK_EQ(bidding_and_scoring_phase_state_, PhaseState::kDuring);
--num_scoring_dependencies_;
// If we issued the final set of bids to a seller worklet, tell it to send any
// pending scoring signals request to complete the auction more quickly.
if (num_scoring_dependencies_ == 0 && ReadyToScoreBids()) {
seller_worklet_handle_->GetSellerWorklet()->SendPendingSignalsRequests();
}
MaybeCompleteBiddingAndScoringPhase();
}
void InterestGroupAuction::ScoreBidIfReady(std::unique_ptr<Bid> bid) {
DCHECK_EQ(bidding_and_scoring_phase_state_, PhaseState::kDuring);
DCHECK(bid);
DCHECK(bid->bid_state->made_bid);
any_bid_made_ = true;
// If seller worklet hasn't been received yet, or configuration is still
// waiting on some promises, wait till everything is ready.
// TODO(morlovich): Tracing doesn't reflect config wait here.
uint64_t bid_trace_id = bid->TraceId();
if (!ReadyToScoreBids()) {
bid->trace_wait_seller_deps_start = base::TimeTicks::Now();
TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("fledge", "wait_for_seller_deps",
bid_trace_id);
unscored_bids_.emplace_back(std::move(bid));
return;
}
TRACE_EVENT_NESTABLE_ASYNC_BEGIN1("fledge", ScoreAdTraceEventName(*bid),
bid_trace_id, "decision_logic_url",
config_->decision_logic_url);
bid->seller_worklet_score_ad_start = base::TimeTicks::Now();
++bids_being_scored_;
Bid* bid_raw = bid.get();
mojo::PendingRemote<auction_worklet::mojom::ScoreAdClient> score_ad_remote;
score_ad_receivers_.Add(
this, score_ad_remote.InitWithNewPipeAndPassReceiver(), std::move(bid));
DCHECK_EQ(0, config_->NumPromises());
SubresourceUrlBuilder* url_builder = SubresourceUrlBuilderIfReady();
DCHECK(url_builder); // Should be ready by now.
seller_worklet_handle_->AuthorizeSubresourceUrls(*url_builder);
seller_worklet_handle_->GetSellerWorklet()->ScoreAd(
bid_raw->ad_metadata, bid_raw->bid, bid_raw->bid_currency,
config_->non_shared_params, GetDirectFromSellerSellerSignals(url_builder),
GetDirectFromSellerSellerSignalsHeaderAdSlot(
*direct_from_seller_signals_header_ad_slot_),
GetDirectFromSellerAuctionSignals(url_builder),
GetDirectFromSellerAuctionSignalsHeaderAdSlot(
*direct_from_seller_signals_header_ad_slot_),
GetOtherSellerParam(*bid_raw),
parent_ ? PerBuyerCurrency(config_->seller, *parent_->config_)
: absl::nullopt,
bid_raw->interest_group->owner, bid_raw->ad_descriptor.url,
bid_raw->GetAdComponentUrls(), bid_raw->bid_duration.InMilliseconds(),
SellerTimeout(), bid_trace_id, std::move(score_ad_remote));
}
bool InterestGroupAuction::ValidateScoreBidCompleteResult(
double score,
auction_worklet::mojom::ComponentAuctionModifiedBidParams*
component_auction_modified_bid_params,
absl::optional<double> bid_in_seller_currency,
const absl::optional<GURL>& debug_loss_report_url,
const absl::optional<GURL>& debug_win_report_url) {
DCHECK_EQ(bidding_and_scoring_phase_state_, PhaseState::kDuring);
// If `debug_loss_report_url` or `debug_win_report_url` is not a valid HTTPS
// URL, the auction should fail because the worklet is compromised.
if (debug_loss_report_url.has_value() &&
!IsUrlValid(debug_loss_report_url.value())) {
score_ad_receivers_.ReportBadMessage(
"Invalid seller debugging loss report URL");
return false;
}
if (debug_win_report_url.has_value() &&
!IsUrlValid(debug_win_report_url.value())) {
score_ad_receivers_.ReportBadMessage(
"Invalid seller debugging win report URL");
return false;
}
// Only validate `component_auction_modified_bid_params` if the bid was
// accepted.
if (score > 0) {
// If they accept a bid / return a positive score, component auction
// SellerWorklets must return a `component_auction_modified_bid_params`,
// and top-level auctions must not.
if ((parent_ == nullptr) !=
(component_auction_modified_bid_params == nullptr)) {
score_ad_receivers_.ReportBadMessage(
"Invalid component_auction_modified_bid_params");
return false;
}
// If a component seller modified the bid, the new bid must also be valid,
// as should its currency.
if (component_auction_modified_bid_params &&
component_auction_modified_bid_params->has_bid) {
if (!IsValidBid(component_auction_modified_bid_params->bid)) {
score_ad_receivers_.ReportBadMessage(
"Invalid component_auction_modified_bid_params bid");
return false;
}
if (!blink::VerifyAdCurrencyCode(
config_->non_shared_params.seller_currency,
component_auction_modified_bid_params->bid_currency) ||
!blink::VerifyAdCurrencyCode(
PerBuyerCurrency(config_->seller, *parent_->config_),
component_auction_modified_bid_params->bid_currency)) {
score_ad_receivers_.ReportBadMessage(
"Invalid component_auction_modified_bid_params bid_currency");
return false;
}
}
}
if (bid_in_seller_currency.has_value() &&
(!IsValidBid(*bid_in_seller_currency) ||
!config_->non_shared_params.seller_currency.has_value())) {
score_ad_receivers_.ReportBadMessage("Invalid bid_in_seller_currency");
return false;
}
return true;
}
void InterestGroupAuction::OnScoreAdComplete(
double score,
auction_worklet::mojom::RejectReason reject_reason,
auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr
component_auction_modified_bid_params,
absl::optional<double> bid_in_seller_currency,
absl::optional<uint32_t> scoring_signals_data_version,
const absl::optional<GURL>& debug_loss_report_url,
const absl::optional<GURL>& debug_win_report_url,
PrivateAggregationRequests pa_requests,
base::TimeDelta scoring_latency,
auction_worklet::mojom::ScoreAdDependencyLatenciesPtr
score_ad_dependency_latencies,
const std::vector<std::string>& errors) {
DCHECK_EQ(bidding_and_scoring_phase_state_, PhaseState::kDuring);
DCHECK_GT(bids_being_scored_, 0);
if (!ValidateScoreBidCompleteResult(
score, component_auction_modified_bid_params.get(),
bid_in_seller_currency, debug_loss_report_url,
debug_win_report_url)) {
OnBiddingAndScoringComplete(AuctionResult::kBadMojoMessage);
return;
}
std::unique_ptr<Bid> bid = std::move(score_ad_receivers_.current_context());
score_ad_receivers_.Remove(score_ad_receivers_.current_receiver());
auction_metrics_recorder_->RecordScoreAdFlowLatency(
base::TimeTicks::Now() - bid->seller_worklet_score_ad_start);
auction_metrics_recorder_->RecordScoreAdLatency(scoring_latency);
auction_metrics_recorder_->RecordScoreAdDependencyLatencies(
*score_ad_dependency_latencies);
TRACE_EVENT_NESTABLE_ASYNC_END0("fledge", ScoreAdTraceEventName(*bid),
bid->TraceId());
if (bid->bid_role == Bid::BidRole::kEnforcedKAnon) {
bid->bid_state->EndTracingKAnonScoring();
} else {
bid->bid_state->EndTracing();
}
bid->bid_state->pa_timings(seller_phase()).script_run_time = scoring_latency;
bid->bid_state->pa_timings(seller_phase()).signals_fetch_time =
score_ad_dependency_latencies->trusted_scoring_signals_latency.value_or(
base::TimeDelta());
--bids_being_scored_;
// Reporting and the like should be done only for things that go into the
// run that produces the result of runAdAuction().
if (IsBidRoleUsedForWinner(kanon_mode_, bid->bid_role)) {
// The mojom API declaration should ensure none of these are null.
DCHECK(base::ranges::none_of(
pa_requests,
[](const auction_worklet::mojom::PrivateAggregationRequestPtr&
request_ptr) { return request_ptr.is_null(); }));
MaybeLogPrivateAggregationWebFeatures(pa_requests);
if (!pa_requests.empty()) {
DCHECK(config_);
BidState::PrivateAggregationPhaseKey agg_key = {
config_->seller, seller_phase(),
config_->aggregation_coordinator_origin};
PrivateAggregationRequests& pa_requests_for_seller =
bid->bid_state->private_aggregation_requests[std::move(agg_key)];
for (auction_worklet::mojom::PrivateAggregationRequestPtr& request :
pa_requests) {
// A for-event private aggregation request with non-reserved event type
// from scoreAd() should be ignored and not reported.
if (request->contribution->is_for_event_contribution() &&
!base::StartsWith(
request->contribution->get_for_event_contribution()->event_type,
"reserved.")) {
continue;
}
pa_requests_for_seller.emplace_back(std::move(request));
}
}
// Use separate fields for component and top-level seller reports, so both
// can send debug reports.
if (bid->auction == this) {
bid->bid_state->seller_debug_loss_report_url =
std::move(debug_loss_report_url);
bid->bid_state->seller_debug_win_report_url =
std::move(debug_win_report_url);
// Ignores reject reason if score > 0.
// TODO(qingxinwu): Set bid_state->reject_reason to nullopt instead of
// kNotAvailable when score > 0.
if (score <= 0) {
bid->bid_state->reject_reason = reject_reason;
}
} else {
bid->bid_state->top_level_seller_debug_loss_report_url =
std::move(debug_loss_report_url);
bid->bid_state->top_level_seller_debug_win_report_url =
std::move(debug_win_report_url);
}
}
// Debug output to developers should include all of errors.
errors_.insert(errors_.end(), errors.begin(), errors.end());
// A score <= 0 means the seller rejected the bid.
if (score > 0) {
switch (bid->bid_role) {
case Bid::BidRole::kUnenforcedKAnon:
UpdateAuctionLeaders(std::move(bid), score,
std::move(component_auction_modified_bid_params),
bid_in_seller_currency,
scoring_signals_data_version,
non_kanon_enforced_auction_leader_);
break;
case Bid::BidRole::kEnforcedKAnon:
UpdateAuctionLeaders(std::move(bid), score,
std::move(component_auction_modified_bid_params),
bid_in_seller_currency,
scoring_signals_data_version,
kanon_enforced_auction_leader_);
break;
case Bid::BidRole::kBothKAnonModes: {
auto bid_copy = std::make_unique<Bid>(*bid);
auto modified_bid_params_copy =
component_auction_modified_bid_params
? component_auction_modified_bid_params->Clone()
: auction_worklet::mojom::
ComponentAuctionModifiedBidParamsPtr();
UpdateAuctionLeaders(std::move(bid), score,
std::move(component_auction_modified_bid_params),
bid_in_seller_currency,
scoring_signals_data_version,
non_kanon_enforced_auction_leader_);
UpdateAuctionLeaders(
std::move(bid_copy), score, std::move(modified_bid_params_copy),
bid_in_seller_currency, scoring_signals_data_version,
kanon_enforced_auction_leader_);
}
}
}
// Need to delete `bid` because OnBiddingAndScoringComplete() may delete
// this, which leaves danging pointers on the stack. While this is safe to
// do (nothing has access to `bid` to dereference them), it makes the
// dangling pointer tooling sad.
bid.reset();
MaybeCompleteBiddingAndScoringPhase();
}
void InterestGroupAuction::UpdateAuctionLeaders(
std::unique_ptr<Bid> bid,
double score,
auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr
component_auction_modified_bid_params,
absl::optional<double> bid_in_seller_currency,
absl::optional<uint32_t> scoring_signals_data_version,
LeaderInfo& leader_info) {
DCHECK_EQ(bidding_and_scoring_phase_state_,
is_server_auction_ ? PhaseState::kBefore : PhaseState::kDuring);
bool is_top_bid = false;
const url::Origin& owner = bid->interest_group->owner;
if (!leader_info.top_bid || score > leader_info.top_bid->score) {
// If there's no previous top bidder, or the bidder has the highest score,
// need to replace the previous top bidder.
is_top_bid = true;
if (leader_info.top_bid) {
OnNewHighestScoringOtherBid(
leader_info.top_bid->score, leader_info.top_bid->bid->bid,
leader_info.top_bid->bid_in_seller_currency,
&leader_info.top_bid->bid->interest_group->owner, leader_info);
}
leader_info.num_top_bids = 1;
leader_info.at_most_one_top_bid_owner = true;
} else if (score == leader_info.top_bid->score) {
// If there's a tie, replace the top-bidder with 1-in-`num_top_bids_`
// chance. This is the select random value from a stream with fixed
// storage problem.
++leader_info.num_top_bids;
if (1 == base::RandInt(1, leader_info.num_top_bids)) {
is_top_bid = true;
}
if (owner != leader_info.top_bid->bid->interest_group->owner) {
leader_info.at_most_one_top_bid_owner = false;
}
// If the top bid is being replaced, need to add the old top bid as a second
// highest bid. Otherwise, need to add the current bid as a second highest
// bid.
double new_highest_scoring_other_bid =
is_top_bid ? leader_info.top_bid->bid->bid : bid->bid;
absl::optional<double> new_highest_scoring_other_bid_in_seller_currency =
is_top_bid ? leader_info.top_bid->bid_in_seller_currency
: bid_in_seller_currency;
OnNewHighestScoringOtherBid(
score, new_highest_scoring_other_bid,
new_highest_scoring_other_bid_in_seller_currency,
leader_info.at_most_one_top_bid_owner ? &bid->interest_group->owner
: nullptr,
leader_info);
} else if (score >= leader_info.second_highest_score) {
// Also use this bid (the most recent one) as highest scoring other bid if
// there's a tie for second highest score.
OnNewHighestScoringOtherBid(score, bid->bid, bid_in_seller_currency, &owner,
leader_info);
}
if (is_top_bid) {
leader_info.top_bid = std::make_unique<ScoredBid>(
score, std::move(scoring_signals_data_version), std::move(bid),
std::move(bid_in_seller_currency),
std::move(component_auction_modified_bid_params));
}
}
void InterestGroupAuction::OnNewHighestScoringOtherBid(
double score,
double bid_value,
absl::optional<double> bid_in_seller_currency,
const url::Origin* owner,
LeaderInfo& leader_info) {
DCHECK_EQ(bidding_and_scoring_phase_state_,
is_server_auction_ ? PhaseState::kBefore : PhaseState::kDuring);
// Current (the most recent) bid becomes highest scoring other bid.
if (score > leader_info.second_highest_score) {
leader_info.highest_scoring_other_bid = bid_value;
leader_info.highest_scoring_other_bid_in_seller_currency =
bid_in_seller_currency;
leader_info.num_second_highest_bids = 1;
// Owner may be false if this is one of the bids tied for first place.
if (!owner) {
leader_info.highest_scoring_other_bid_owner.reset();
} else {
leader_info.highest_scoring_other_bid_owner = *owner;
}
leader_info.second_highest_score = score;
return;
}
DCHECK_EQ(score, leader_info.second_highest_score);
if (!owner || *owner != leader_info.highest_scoring_other_bid_owner) {
leader_info.highest_scoring_other_bid_owner.reset();
}
++leader_info.num_second_highest_bids;
// In case of a tie, randomly pick one. This is the select random value from a
// stream with fixed storage problem.
if (1 == base::RandInt(1, leader_info.num_second_highest_bids)) {
leader_info.highest_scoring_other_bid = bid_value;
leader_info.highest_scoring_other_bid_in_seller_currency =
bid_in_seller_currency;
}
}
absl::optional<base::TimeDelta> InterestGroupAuction::SellerTimeout() {
if (config_->non_shared_params.seller_timeout.has_value()) {
return std::min(config_->non_shared_params.seller_timeout.value(),
kMaxPerBuyerTimeout);
}
return absl::nullopt;
}
void InterestGroupAuction::MaybeCompleteBiddingAndScoringPhase() {
DCHECK_EQ(bidding_and_scoring_phase_state_, PhaseState::kDuring);
if (!IsBiddingAndScoringPhaseComplete()) {
return;
}
all_bids_scored_ = true;
AuctionResult result = AuctionResult::kSuccess;
// If there's no winning bid, fail with kAllBidsRejected if there were any
// bids. Otherwise, fail with kNoBids or kNoInterestGroups.
if (!top_bid()) {
if (any_bid_made_) {
result = AuctionResult::kAllBidsRejected;
} else {
result = HasInterestGroups() ? AuctionResult::kNoBids
: AuctionResult::kNoInterestGroups;
}
}
// This needs to be asynchronous since it can happens inside
// StartBiddingAndScoringPhase if there is actually nothing to do, and we
// don't want to delete the auction while we're in process of starting it.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&InterestGroupAuction::OnBiddingAndScoringComplete,
weak_ptr_factory_.GetWeakPtr(), result,
std::vector<std::string>()));
}
void InterestGroupAuction::OnBiddingAndScoringComplete(
AuctionResult auction_result,
const std::vector<std::string>& errors) {
DCHECK(bidding_and_scoring_phase_callback_);
DCHECK(!final_auction_result_);
DCHECK_EQ(bidding_and_scoring_phase_state_,
is_server_auction_ ? PhaseState::kBefore : PhaseState::kDuring);
bidding_and_scoring_phase_state_ = PhaseState::kAfter;
TRACE_EVENT_NESTABLE_ASYNC_END0("fledge", "bidding_and_scoring_phase",
*trace_id_);
errors_.insert(errors_.end(), errors.begin(), errors.end());
// If this is a component auction, have to unload the seller worklet handle to
// avoid deadlock. Otherwise, loading the top-level seller worklet may be
// blocked by component seller worklets taking up all the quota.
if (parent_) {
seller_worklet_handle_.reset();
}
// If the seller loaded callback hasn't been invoked yet, call it now. This is
// needed in the case the phase ended without receiving the seller worklet
// (e.g., in the case no bidder worklet bids).
if (on_seller_receiver_callback_) {
std::move(on_seller_receiver_callback_).Run();
}
bool success = auction_result == AuctionResult::kSuccess;
if (!success) {
// Close all pipes, to prevent any pending callbacks from being invoked if
// this phase is being completed due to a fatal error, like the seller
// worklet failing to load.
ClosePipes();
// `final_auction_result_` should only be set to kSuccess when the entire
// auction is complete.
final_auction_result_ = auction_result;
} else {
// If this is the top-level auction, mark it as a success (and the winning
// component auction as well, if there is one). Component auction status
// depend on whether or not they won the top-level auction, so if this is a
// component auction, leave status as-is.
if (!parent_) {
final_auction_result_ = AuctionResult::kSuccess;
// If there's a winning bid, set its auction result as well. If the
// winning bid came from a component auction, this will set that component
// auction's result as well. This is needed for auction result accessors.
//
// TODO(https://crbug.com/1394777): This is currently needed to correctly
// retrieve reporting information from the nested auction. Is there a
// cleaner way to do this?
if (top_bid()) {
top_bid()->bid->auction->final_auction_result_ =
AuctionResult::kSuccess;
}
}
}
// If this is a top-level auction with component auction, update final state
// of all successfully completed component auctions with bids that did not win
// to reflect a loss.
for (auto& component_auction_info : component_auctions_) {
InterestGroupAuction* component_auction =
component_auction_info.second.get();
// Leave the state of the winning component auction alone, if the winning
// bid is from a component auction.
ScoredBid* winner = top_bid();
if (winner && winner->bid->auction == component_auction) {
continue;
}
if (component_auction->final_auction_result_) {
continue;
}
component_auction->final_auction_result_ =
AuctionResult::kComponentLostAuction;
}
std::move(bidding_and_scoring_phase_callback_).Run(success);
}
auction_worklet::mojom::ComponentAuctionOtherSellerPtr
InterestGroupAuction::GetOtherSellerParam(const Bid& bid) const {
auction_worklet::mojom::ComponentAuctionOtherSellerPtr
browser_signals_other_seller;
if (parent_) {
// This is a component seller scoring a bid from its own auction.
// Need to provide the top-level seller origin.
browser_signals_other_seller =
auction_worklet::mojom::ComponentAuctionOtherSeller::NewTopLevelSeller(
parent_->config_->seller);
} else if (bid.auction != this) {
// This is a top-level seller scoring a bid from a component auction.
// Need to provide the component seller origin.
browser_signals_other_seller =
auction_worklet::mojom::ComponentAuctionOtherSeller::NewComponentSeller(
bid.auction->config_->seller);
}
return browser_signals_other_seller;
}
AuctionWorkletManager::WorkletKey InterestGroupAuction::BidderWorkletKey(
BidState& bid_state) {
DCHECK(!bid_state.worklet_handle);
DCHECK(!bid_state.additional_bid_buyer);
const blink::InterestGroup& interest_group = bid_state.bidder->interest_group;
absl::optional<uint16_t> experiment_group_id =
GetBuyerExperimentId(*config_, interest_group.owner);
return AuctionWorkletManager::BidderWorkletKey(
interest_group.bidding_url.value_or(GURL()),
interest_group.bidding_wasm_helper_url,
interest_group.trusted_bidding_signals_url,
/*needs_cors_for_additional_bid=*/false, experiment_group_id);
}
void InterestGroupAuction::OnDecompressedServerResponse(
AdAuctionRequestContext* request_context,
base::expected<mojo_base::BigBuffer, std::string> result) {
if (!result.has_value()) {
// We couldn't unzip the response.
OnBiddingAndScoringComplete(
AuctionResult::kInvalidServerResponse,
{"runAdAuction(): Could not decompress server response"});
return;
}
base::span<const uint8_t> result_span = result.value().byte_span();
data_decoder_->ParseCbor(
result_span,
base::BindOnce(
&InterestGroupAuction::OnParsedServerResponse,
weak_ptr_factory_.GetWeakPtr(),
// This use of unretained is safe since the request_context is owned
// by a PageUserData and is only deleted when the page is destroyed.
// This auction is owned by the page through a DocumentService, so it
// will be destroyed before the decoder.
base::Unretained(request_context)));
// `result` falls out of scope, deallocating the buffer with the decompressed
// response. This is okay because `DataDecoder::ParseCbor` made a copy before
// it returned (via implicit construction of mojo_base::BigBuffer).
}
void InterestGroupAuction::OnParsedServerResponse(
AdAuctionRequestContext* request_context,
data_decoder::DataDecoder::ValueOrError result) {
if (!result.has_value()) {
// We couldn't parse the CBOR of the response.
OnBiddingAndScoringComplete(
AuctionResult::kInvalidServerResponse,
{"runAdAuction(): Could not parse server response"});
return;
}
absl::optional<BiddingAndAuctionResponse> response =
BiddingAndAuctionResponse::TryParse(std::move(result).value(),
request_context->group_names);
if (!response) {
// We couldn't recognize the structure of the response.
OnBiddingAndScoringComplete(
AuctionResult::kInvalidServerResponse,
{"runAdAuction(): Could not parse server response"});
return;
}
std::vector<std::string> errors;
if (response->error) {
errors.push_back(base::StrCat({"runAdAuction(): ", *response->error}));
}
if (response->is_chaff) {
// This is a place-holder response because there was no winner.
OnBiddingAndScoringComplete(AuctionResult::kNoBids, errors);
return;
}
if (response->bid && !IsValidBid(*response->bid)) {
errors.push_back(base::StrCat({"runAdAuction(): Invalid bid value ",
base::NumberToString(*response->bid)}));
OnBiddingAndScoringComplete(AuctionResult::kNoBids, errors);
return;
}
any_bid_made_ = !response->bidding_groups.empty();
blink::InterestGroupKey winning_group(response->interest_group_owner,
response->interest_group_name);
// Winning group must be a bidder.
if (!base::Contains(response->bidding_groups, winning_group)) {
errors.push_back("runAdAuction(): Winning group must be a bidder");
OnBiddingAndScoringComplete(AuctionResult::kInvalidServerResponse, errors);
return;
}
interest_group_manager_->GetInterestGroup(
winning_group, base::BindOnce(&InterestGroupAuction::OnLoadedWinningGroup,
weak_ptr_factory_.GetWeakPtr(),
std::move(response).value()));
}
void InterestGroupAuction::OnLoadedWinningGroup(
BiddingAndAuctionResponse response,
absl::optional<StorageInterestGroup> maybe_group) {
if (!maybe_group) {
OnBiddingAndScoringComplete(
AuctionResult::kInvalidServerResponse,
{"runAdAuction(): Could not load winning interest group"});
return;
}
std::vector<blink::AdDescriptor> ad_components;
base::ranges::transform(
response.ad_components, std::back_inserter(ad_components),
[](const GURL& url) { return blink::AdDescriptor(url); });
if (!maybe_group->interest_group.bidding_url) {
// Groups must have a bidding logic URL to bid.
OnBiddingAndScoringComplete(
AuctionResult::kInvalidServerResponse,
{"runAdAuction(): Winning group doesn't have a bidding URL"});
return;
}
all_bids_scored_ = true;
std::vector<SingleStorageInterestGroup> groups;
groups.emplace_back(std::move(*maybe_group));
auto buyer_helper = std::make_unique<BuyerHelper>(this, std::move(groups));
std::unique_ptr<Bid> bid = buyer_helper->TryToCreateBidFromServerResponse(
Bid::BidRole::kUnenforcedKAnon, // TODO(behamilton): Fix this.
response.bid.value_or(0.00001),
/*ad_descriptor=*/
blink::AdDescriptor(response.ad_render_url),
/*ad_component_descriptors=*/std::move(ad_components));
buyer_helpers_.emplace_back(std::move(buyer_helper));
if (!bid) {
OnBiddingAndScoringComplete(
AuctionResult::kInvalidServerResponse,
{"runAdAuction(): Couldn't reconstruct winning bid"});
return;
}
SubresourceUrlBuilderIfReady(); // This is required for some reason.
UpdateAuctionLeaders(
std::move(bid), response.score.value_or(0.00001),
// TODO(1457241): Use correct modified bid params for multi-level auction.
/*component_auction_modified_bid_params=*/
nullptr,
/*bid_in_seller_currency=*/absl::nullopt,
/*scoring_signals_data_version=*/absl::nullopt,
// TODO(behamilton): Properly support k-anonymity here
non_kanon_enforced_auction_leader_);
saved_response_ = std::move(response);
OnBiddingAndScoringComplete(AuctionResult::kSuccess);
}
void InterestGroupAuction::OnDirectFromSellerSignalHeaderAdSlotResolved(
std::unique_ptr<HeaderDirectFromSellerSignals> signals,
std::vector<std::string> errors) {
DCHECK_NE(bidding_and_scoring_phase_state_, PhaseState::kAfter);
CHECK(direct_from_seller_signals_header_ad_slot_pending_);
CHECK(direct_from_seller_signals_header_ad_slot_);
direct_from_seller_signals_header_ad_slot_ = std::move(signals);
errors_.insert(errors_.end(), errors.begin(), errors.end());
direct_from_seller_signals_header_ad_slot_pending_ = false;
if (bidding_and_scoring_phase_state_ == PhaseState::kDuring) {
for (const auto& buyer_helper : buyer_helpers_) {
buyer_helper->NotifyConfigDependencyResolved();
}
OnScoringDependencyDone();
ScoreQueuedBidsIfReady();
}
}
// static
data_decoder::DataDecoder* InterestGroupAuction::GetDataDecoder(
base::WeakPtr<InterestGroupAuction> instance) {
if (!instance) {
return nullptr;
}
return instance->data_decoder_.get();
}
} // namespace content