Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
duyuefeng0708
GitHub Repository: duyuefeng0708/Cryptography-From-First-Principle
Path: blob/main/frontier/12-mpc/connect/secure-auctions.ipynb
483 views
unlisted
Kernel: SageMath 10.0

Connect: Secure Auctions with MPC

Module 12 | Real-World Connections

Compute the winning bid without revealing any losing bids.

Introduction

In a sealed-bid auction, each bidder submits a secret bid. The auctioneer determines the highest bid (first-price) or second-highest bid (Vickrey auction) and announces the winner.

The problem: a traditional auctioneer sees all bids. This creates opportunities for manipulation --- the auctioneer could favor a bidder, leak bid amounts, or even fabricate bids.

MPC-based auctions eliminate the trusted auctioneer. Bidders secret-share their bids among a set of computation servers. The servers jointly evaluate a comparison circuit on the shared inputs to find the maximum, without any server learning any bid.

The first real-world MPC deployment was exactly this: the Danish sugar beet auction in 2008, where farmers submitted secret supply bids to determine market prices.

Toy Setup: 4 Bidders, Additive Sharing

We simulate a sealed-bid auction with 4 bidders and 3 computation servers. Each bidder splits their bid into additive shares distributed to the servers. The servers then compute comparisons on the shared values to find the maximum.

# === Setup: additive sharing among computation servers === p = 1009 F = GF(p) def additive_share(secret, n, field): """Split secret into n additive shares.""" p_val = field.order() shares = [field(randint(0, p_val - 1)) for _ in range(n - 1)] shares.append(field(secret) - sum(shares)) return shares def additive_reconstruct(shares): """Reconstruct by summing all shares.""" return sum(shares) # Auction parameters n_servers = 3 bidders = ["Alice", "Bob", "Carol", "Dave"] bids = [F(150), F(280), F(210), F(320)] # secret bids print(f"=== Sealed-Bid Auction ===") print(f"Bidders: {bidders}") print(f"Secret bids (known only to each bidder):") for name, bid in zip(bidders, bids): print(f" {name}: {bid}") print(f"\nComputation servers: {n_servers}") print(f"True maximum: {max(bids)} (Dave)")

Step 1: Bidders Share Their Bids

Each bidder splits their bid into additive shares and distributes one share to each computation server. After this step, no server knows any individual bid.

# === Step 1: Each bidder shares their bid === # bid_shares[i][j] = server j's share of bidder i's bid bid_shares = [] for i, (name, bid) in enumerate(zip(bidders, bids)): shares = additive_share(bid, n_servers, F) bid_shares.append(shares) print("Shares distributed to servers:") print("Bidder", end="")for j in range(n_servers): print(f"{'Server '+str(j+1)}", end="") print() for i, name in enumerate(bidders): print(f"{name}", end="") for j in range(n_servers): print(f"{str(bid_shares[i][j])}", end="") print() # Verify shares reconstruct correctly print(f"\nReconstruction check:") for i, name in enumerate(bidders): rec = additive_reconstruct(bid_shares[i]) print(f" {name}: {rec} {'correct' if rec == bids[i] else 'WRONG'}")

Step 2: Secure Comparison Using Beaver Triples

To find the maximum bid, the servers need to compare pairs of shared values. Comparing [a][a] vs [b][b] means computing the sign of [ab][a - b], which requires bit decomposition and multiplications on shared values.

For our toy example, we use a simplified comparison protocol that works for small values: compute [ab][a - b] and open it (with random masking to prevent leaking the exact difference).

# === Step 2: Pairwise comparison on shared values === def secure_compare(shares_a, shares_b, field): """Compare two shared values: return True if a > b. Simplified protocol: compute [a - b], mask with random r, open r * (a - b). The sign is preserved if r > 0 (in the 'small values' regime where a - b fits in [-p/2, p/2]). In a real protocol, this would use bit decomposition. """ n = len(shares_a) p_val = field.order() # Compute shares of (a - b) shares_diff = [shares_a[i] - shares_b[i] for i in range(n)] # Open the difference (in a real protocol, this would be masked) diff = additive_reconstruct(shares_diff) # Interpret as signed: values > p/2 are negative diff_int = ZZ(diff) if diff_int > p_val // 2: diff_int -= p_val return diff_int > 0 def secure_max(all_shares, field): """Find the index of the maximum shared value using pairwise comparisons. Returns (winner_index, comparison_log). """ n_bids = len(all_shares) log = [] # Tournament: track the current maximum max_idx = 0 for i in range(1, n_bids): i_wins = secure_compare(all_shares[i], all_shares[max_idx], field) log.append((i, max_idx, i_wins)) if i_wins: max_idx = i return max_idx, log # Run the secure comparison tournament winner_idx, comp_log = secure_max(bid_shares, F) print("=== Secure Comparison Tournament ===") print() for i, j, i_wins in comp_log: winner = bidders[i] if i_wins else bidders[j] print(f" {bidders[i]} vs {bidders[j]}: {winner} has the higher bid") print(f"\nWinner: {bidders[winner_idx]}") print(f"Correct? {bidders[winner_idx] == 'Dave'}")

Step 3: Reveal Only the Winning Bid

The servers open the winner's shared bid. All losing bids remain secret --- their shares are never reconstructed.

# === Step 3: Open only the winning bid === winning_bid = additive_reconstruct(bid_shares[winner_idx]) print(f"=== Auction Result ===") print(f"Winner: {bidders[winner_idx]}") print(f"Winning bid: {winning_bid}") print() print(f"Losing bids remain SECRET:") for i, name in enumerate(bidders): if i == winner_idx: print(f" {name}: {winning_bid} (revealed)") else: print(f" {name}: [HIDDEN] (shares not reconstructed)") print(f"\nNo server learned any individual bid during the computation.") print(f"The comparison was performed entirely on shared values.")

Beaver Triples for Secure Multiplication

In a real secure auction, comparisons require multiplications on shared bits. Each multiplication consumes one Beaver triple (from Notebook 12b).

Here we demonstrate that Beaver triples enable multiplication on shared values, which is the building block for the comparison circuit.

# === Beaver triples for secure multiplication === def beaver_multiply(shares_a, shares_b, shares_alpha, shares_beta, shares_gamma, field): """Multiply two shared values using a Beaver triple.""" n_parties = len(shares_a) shares_eps = [shares_a[i] - shares_alpha[i] for i in range(n_parties)] shares_del = [shares_b[i] - shares_beta[i] for i in range(n_parties)] epsilon = sum(shares_eps) delta = sum(shares_del) shares_product = [] for i in range(n_parties): share_i = shares_gamma[i] + epsilon * shares_beta[i] + delta * shares_alpha[i] if i == 0: share_i += epsilon * delta shares_product.append(share_i) return shares_product # Demonstrate: multiply two shared bid values # (In a comparison circuit, you'd multiply shared bits, not full bids) a_val, b_val = F(280), F(210) sh_a = additive_share(a_val, n_servers, F) sh_b = additive_share(b_val, n_servers, F) # Generate a Beaver triple alpha = F.random_element() beta = F.random_element() gamma = alpha * beta sh_alpha = additive_share(alpha, n_servers, F) sh_beta = additive_share(beta, n_servers, F) sh_gamma = additive_share(gamma, n_servers, F) sh_product = beaver_multiply(sh_a, sh_b, sh_alpha, sh_beta, sh_gamma, F) result = additive_reconstruct(sh_product) print(f"Beaver triple multiplication:") print(f" [a] * [b] = [{a_val}] * [{b_val}] = [{result}]") print(f" Expected: {a_val * b_val}") print(f" Correct? {result == a_val * b_val}") print(f"\nEach comparison in the auction uses several such multiplications") print(f"on shared bits for the bit-decomposed comparison circuit.")
# === Vickrey (second-price) auction variant === def secure_second_max(all_shares, field): """Find indices of the top two shared values.""" n_bids = len(all_shares) # Simple tournament approach first_idx = 0 second_idx = -1 for i in range(1, n_bids): i_beats_first = secure_compare(all_shares[i], all_shares[first_idx], field) if i_beats_first: second_idx = first_idx first_idx = i elif second_idx == -1 or secure_compare(all_shares[i], all_shares[second_idx], field): second_idx = i return first_idx, second_idx first_idx, second_idx = secure_second_max(bid_shares, F) second_price = additive_reconstruct(bid_shares[second_idx]) print("=== Vickrey (Second-Price) Auction ===") print(f"Winner: {bidders[first_idx]} (highest bidder)") print(f"Price paid: {second_price} (second-highest bid, from {bidders[second_idx]})") print() print(f"In a Vickrey auction, the winner pays the SECOND highest price.") print(f"This incentivizes truthful bidding (bidding your true value is optimal).") print(f"MPC ensures no one learns the actual bids, only the winner and the price.")

Real-World MPC Auctions

DeploymentYearDetails
Danish sugar beet auction2008First real MPC deployment. Farmers submitted supply bids; market-clearing price computed securely. Used Shamir sharing with 3 servers.
Spectrum auctionsResearchGovernments auction radio spectrum licenses; MPC prevents bid manipulation by the auctioneer.
Procurement auctionsOngoingCompanies submit sealed bids for contracts; MPC ensures fairness.
Dark pool tradingResearchMatch buy/sell orders without revealing prices to the exchange operator.

The Danish sugar beet auction demonstrated that MPC is practical: it processed real economic transactions with real money, using a protocol based on Shamir sharing among three servers (one operated by each stakeholder group).

Concept Map: Module 12 \to Secure Auctions

Module 12 ConceptAuction Application
Additive secret sharingEach bidder splits their bid into shares for the servers
Beaver triplesEnable multiplication gates in the comparison circuit
Secure comparisonDetermine which of two shared bids is larger
MPC circuit evaluationCompute max() or second-max() on shared inputs
SPDZ MACsPrevent a malicious server from biasing the auction result
Oblivious transferUsed in the offline phase to generate Beaver triples
# === Exercise: Auction with more bidders === random.seed(99) n_bidders = 8 exercise_bidders = [f"Bidder_{i+1}" for i in range(n_bidders)] exercise_bids = [F(randint(50, 500)) for _ in range(n_bidders)] print(f"=== {n_bidders}-Bidder Auction ===") print(f"(Bids shown here for verification, servers never see them)") print() for name, bid in zip(exercise_bidders, exercise_bids): print(f" {name}: {bid}") # Share all bids exercise_shares = [additive_share(bid, n_servers, F) for bid in exercise_bids] # Find winner ex_winner, ex_log = secure_max(exercise_shares, F) ex_winning_bid = additive_reconstruct(exercise_shares[ex_winner]) # Verify against plaintext true_max_idx = max(range(n_bidders), key=lambda i: ZZ(exercise_bids[i])) print(f"\nMPC result: {exercise_bidders[ex_winner]} wins with bid {ex_winning_bid}") print(f"True max: {exercise_bidders[true_max_idx]} with bid {exercise_bids[true_max_idx]}") print(f"Correct? {ex_winner == true_max_idx}") print(f"\nNumber of comparisons: {len(ex_log)} (= n_bidders - 1)") print(f"Each comparison uses O(log p) multiplications in a real bit-decomposition circuit.")

Summary

ConceptKey idea
Secret-shared bidsEach bidder splits their bid into additive shares among computation servers
Comparison circuitServers evaluate a secure comparison on shared values to find the maximum
Beaver triplesEnable the multiplications needed for bit-level comparisons
Selective revealOnly the winning bid (and winner identity) is opened; losing bids stay secret
SPDZ MACsPrevent any server from cheating to manipulate the auction outcome

The Danish sugar beet auction (2008) proved this works in practice with real economic stakes. Since then, MPC-based auctions have expanded to spectrum allocation, procurement, and financial trading.


Back to Module 12: Multi-Party Computation