Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(marketplace): implement auction results calculation logic #2747

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 86 additions & 29 deletions marketplace-solver/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use url::Url;

use async_trait::async_trait;
use committable::Committable;
use espresso_types::{
v0_99::{
BidTx, RollupRegistration, RollupRegistrationBody, RollupUpdate, RollupUpdatebody,
BidTx, NamespaceId, RollupRegistration, RollupRegistrationBody, RollupUpdate, RollupUpdatebody,
SolverAuctionResults,
},
PubKey, SeqTypes,
Expand Down Expand Up @@ -259,46 +260,102 @@ impl UpdateSolverState for GlobalState {
.collect::<SolverResult<Vec<RollupRegistration>>>()
}

/// Calculates auction results for permissionless auctions.
///
/// The auction winner selection process:
/// 1. Groups all bids by namespace
/// 2. For each namespace, selects the bid with the highest amount
/// 3. For namespaces without bids, includes reserve URLs if available
///
/// # Arguments
/// * `view_number` - The view number for which to calculate auction results
///
/// # Returns
/// * `SolverResult<SolverAuctionResults>` - The auction results containing winning bids and reserve URLs
async fn calculate_auction_results_permissionless(
&self,
view_number: ViewNumber,
) -> SolverResult<SolverAuctionResults> {
// todo (ab): actual logic needs to implemented
// for we, we just return some default results
// Get all bids for this view
let bids = self
.solver
.bid_txs
.get(&view_number)
.map(|bids| bids.values().cloned().collect::<Vec<_>>())
.unwrap_or_default();

// Group bids by namespace
let mut namespace_bids: HashMap<NamespaceId, Vec<&BidTx>> = HashMap::new();
for bid in bids.iter() {
for namespace in &bid.body.namespaces {
namespace_bids
.entry(*namespace)
.or_default()
.push(bid);
}
}

// Get all rollup registrations to check reserve prices
let rollups = self.get_all_rollup_registrations().await?;
let reserve_prices: HashMap<NamespaceId, u64> = rollups
.iter()
.map(|r| (r.body.namespace_id, r.body.reserve_price))
.collect();

// Select winning bids (highest bid for each namespace that meets reserve price)
let mut winning_bids = HashSet::new();
for (namespace_id, bids) in namespace_bids.iter() {
if let Some(highest_bid) = bids.iter().max_by_key(|bid| bid.body.bid_amount) {
// Only select bid if it meets the reserve price
if let Some(reserve_price) = reserve_prices.get(namespace_id) {
if highest_bid.body.bid_amount >= *reserve_price {
winning_bids.insert((*highest_bid).clone());
}
}
}
}

let results = SolverAuctionResults::new(
// Get reserve URLs for namespaces without winning bids
let reserve_bids: Vec<(NamespaceId, Url)> = rollups
.into_iter()
.filter_map(|r| {
let namespace_id = r.body.namespace_id;
// Only include reserve URL if there are no winning bids for this namespace
// and the rollup is active
if !namespace_bids.contains_key(&namespace_id) && r.body.active {
r.body.reserve_url.map(|url| (namespace_id, url))
} else {
None
}
})
.collect();

Ok(SolverAuctionResults::new(
view_number,
Vec::new(),
rollups
.into_iter()
.filter_map(|r| Some((r.body.namespace_id, r.body.reserve_url?)))
.collect(),
);

Ok(results)
winning_bids.into_iter().collect(),
reserve_bids,
))
}

/// Calculates auction results for permissioned auctions.
///
/// This method uses the same logic as permissionless auctions since signature verification
/// is handled at a higher level. The signature parameter is used for authentication only.
///
/// # Arguments
/// * `view_number` - The view number for which to calculate auction results
/// * `_signature` - The signature key for authentication
///
/// # Returns
/// * `SolverResult<SolverAuctionResults>` - The auction results containing winning bids and reserve URLs
async fn calculate_auction_results_permissioned(
&self,
view_number: ViewNumber,
_signauture: <SeqTypes as NodeType>::SignatureKey,
_signature: <SeqTypes as NodeType>::SignatureKey,
) -> SolverResult<SolverAuctionResults> {
// todo (ab): actual logic needs to implemented
// for we, we just return some default results

let rollups = self.get_all_rollup_registrations().await?;

let results = SolverAuctionResults::new(
view_number,
Vec::new(),
rollups
.into_iter()
.filter_map(|r| Some((r.body.namespace_id, r.body.reserve_url?)))
.collect(),
);

Ok(results)
// For permissioned auctions, we use the same logic as permissionless
// since the signature verification is handled at a higher level
self.calculate_auction_results_permissionless(view_number).await
}
}

Expand Down
146 changes: 143 additions & 3 deletions marketplace-solver/src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,16 @@ mod test {
use committable::Committable;
use espresso_types::{
v0_99::{
BidTx, RollupRegistration, RollupRegistrationBody, RollupUpdate, RollupUpdatebody,
BidTx, BidTxBody, RollupRegistration, RollupRegistrationBody, RollupUpdate, RollupUpdatebody,
},
FeeAccount, MarketplaceVersion, SeqTypes,
Update::{Set, Skip},
};
use hotshot::types::{BLSPubKey, SignatureKey};
use hotshot_types::traits::node_implementation::NodeType;
use hotshot_types::{data::ViewNumber, traits::node_implementation::NodeType};
use tide_disco::Url;

use crate::{testing::MockSolver, SolverError};
use crate::{testing::MockSolver, SolverError, state::UpdateSolverState};

async fn register_rollup_helper(
namespace_id: u64,
Expand Down Expand Up @@ -588,4 +588,144 @@ mod test {
assert_eq!(result[0], reg_ns_1);
assert_eq!(result[1], reg_ns_2);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_calculate_auction_results_no_bids() {
let mock_solver = MockSolver::init().await;
let state = mock_solver.state();
let state = state.read().await;
let view_number = ViewNumber::new(1);

let results = state
.calculate_auction_results_permissionless(view_number)
.await
.unwrap();

assert!(results.winning_bids.is_empty());
assert!(results.reserve_bids.is_empty());
}

#[tokio::test(flavor = "multi_thread")]
async fn test_calculate_auction_results_with_bids() {
let mock_solver = MockSolver::init().await;
let state = mock_solver.state();
let mut state = state.write().await;
let view_number = ViewNumber::new(1);

// Create test namespace and bid
let namespace_id = 1u64.into();
let bid_amount = 1000;

let bid_tx = BidTx {
body: BidTxBody {
view: view_number,
namespaces: vec![namespace_id],
bid_amount,
..Default::default()
},
signature: Default::default(),
};

// Submit bid
state.submit_bid_tx(bid_tx.clone()).await.unwrap();

// Register rollup with reserve price
let (registration, ..) = register_rollup_helper(
1, // namespace_id
Some("http://example.com"),
500, // reserve_price (lower than bid amount)
true, // active
"test rollup"
).await;

state.register_rollup(registration).await.unwrap();

let results = state
.calculate_auction_results_permissionless(view_number)
.await
.unwrap();

assert_eq!(results.winning_bids.len(), 1);
assert_eq!(results.winning_bids[0], bid_tx);
assert!(results.reserve_bids.is_empty());
}

#[tokio::test(flavor = "multi_thread")]
async fn test_calculate_auction_results_bid_below_reserve() {
let mock_solver = MockSolver::init().await;
let state = mock_solver.state();
let mut state = state.write().await;
let view_number = ViewNumber::new(1);

// Create test namespace and bid
let namespace_id = 1u64.into();
let bid_amount = 100;

let bid_tx = BidTx {
body: BidTxBody {
view: view_number,
namespaces: vec![namespace_id],
bid_amount,
..Default::default()
},
signature: Default::default(),
};

// Submit bid
state.submit_bid_tx(bid_tx).await.unwrap();

// Register rollup with higher reserve price
let (registration, ..) = register_rollup_helper(
1, // namespace_id
Some("http://example.com"),
500, // reserve_price (higher than bid amount)
true, // active
"test rollup"
).await;

state.register_rollup(registration).await.unwrap();

let results = state
.calculate_auction_results_permissionless(view_number)
.await
.unwrap();

// Bid should not win as it's below reserve price
assert!(results.winning_bids.is_empty());
// Reserve URL should be included
assert_eq!(results.reserve_bids.len(), 1);
assert_eq!(
results.reserve_bids[0],
(namespace_id, Url::parse("http://example.com").unwrap())
);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_calculate_auction_results_inactive_rollup() {
let mock_solver = MockSolver::init().await;
let state = mock_solver.state();
let mut state = state.write().await;
let view_number = ViewNumber::new(1);

// Register inactive rollup
let (registration, ..) = register_rollup_helper(
1, // namespace_id
Some("http://example.com"),
500, // reserve_price
false, // inactive
"test rollup"
).await;

state.register_rollup(registration).await.unwrap();

let results = state
.calculate_auction_results_permissionless(view_number)
.await
.unwrap();

// No winning bids
assert!(results.winning_bids.is_empty());
// No reserve URLs for inactive rollup
assert!(results.reserve_bids.is_empty());
}
}