Created
January 5, 2026 10:27
-
-
Save m0wer/23d09c90b7a23070a3e4e340b26d6d14 to your computer and use it in GitHub Desktop.
JoinMarket Fidelity Bond Exponent Analysis
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| JoinMarket Fidelity Bond Exponent Analysis | |
| Using real orderbook data to evaluate Sybil attack scenarios | |
| """ | |
| import numpy as np | |
| import pandas as pd | |
| from dataclasses import dataclass | |
| from typing import List | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| # Real JoinMarket orderbook data (locked BTC amounts) | |
| REAL_ORDERBOOK = [ | |
| 30.05401340, 26.55225585, 21.65341699, 20.00996535, 8.08486780, | |
| 6.04915008, 5.39734023, 4.40845980, 4.09837043, 3.51949732, | |
| 3.00192447, 2.67220751, 2.04048986, 1.31983686, 1.00000000, | |
| 0.65404050, 0.63046190, 0.50120445, 0.42555395, 0.41538767, | |
| 0.29488531, 0.23975233, 0.20990883, 0.20575056, 0.19998701, | |
| 0.11764748, 0.09999856, 0.07502071, 0.06301196, 0.04999196, | |
| 0.04132994, 0.02607606, 0.01241268, 0.01106880, 0.00980906, | |
| 0.00946399, 0.00499853, 0.00355610, 0.00327513, 0.00129700, | |
| 0.00108267, 0.00070980, 0.00036572, 0.00029890 | |
| ] | |
| @dataclass | |
| class AttackScenario: | |
| name: str | |
| honest_bonds: List[float] | |
| attacker_coins: float | |
| taker_peers: int | |
| description: str | |
| def bond_value(coins: float, exponent: float) -> float: | |
| return coins**exponent | |
| def simulate_selection( | |
| honest_bonds: List[float], | |
| attacker_bonds: List[float], | |
| num_peers: int, | |
| exponent: float, | |
| iterations: int = 10000, | |
| ) -> dict: | |
| """Monte Carlo simulation of taker peer selection without replacement""" | |
| full_successes = 0 | |
| attacker_peer_counts = [] | |
| for _ in range(iterations): | |
| pool = [ | |
| {"value": bond_value(b, exponent), "honest": True} for b in honest_bonds | |
| ] | |
| pool += [ | |
| {"value": bond_value(b, exponent), "honest": False} for b in attacker_bonds | |
| ] | |
| attacker_selected = 0 | |
| for _ in range(min(num_peers, len(pool))): | |
| total = sum(p["value"] for p in pool) | |
| rand = np.random.random() * total | |
| cumulative = 0 | |
| for i, peer in enumerate(pool): | |
| cumulative += peer["value"] | |
| if rand <= cumulative: | |
| if not peer["honest"]: | |
| attacker_selected += 1 | |
| pool.pop(i) | |
| break | |
| attacker_peer_counts.append(attacker_selected) | |
| if attacker_selected == num_peers: | |
| full_successes += 1 | |
| return { | |
| "full_success_rate": (full_successes / iterations) * 100, | |
| "avg_attacker_peers": np.mean(attacker_peer_counts), | |
| "median_attacker_peers": np.median(attacker_peer_counts), | |
| "attacker_majority_rate": (sum(1 for c in attacker_peer_counts if c > num_peers/2) / iterations) * 100 | |
| } | |
| def analyze_attack_scenario( | |
| scenario: AttackScenario, exponents: List[float] | |
| ) -> pd.DataFrame: | |
| """Analyze attack success across exponents for a scenario""" | |
| results = [] | |
| for exp in exponents: | |
| # Test a wide range of splitting strategies to find the true optimal | |
| # According to AdamISZ: attacker should split into exactly N pieces where N = taker_peers | |
| # But we test more to verify this empirically | |
| max_splits = min(20, scenario.taker_peers * 3) | |
| splits_to_test = list(range(1, max_splits + 1)) | |
| best_result = None | |
| best_full_success = 0 | |
| best_split = None | |
| for n_splits in splits_to_test: | |
| attacker_splits = [scenario.attacker_coins / n_splits] * n_splits | |
| result = simulate_selection( | |
| scenario.honest_bonds, attacker_splits, scenario.taker_peers, exp, iterations=10000 | |
| ) | |
| if result["full_success_rate"] > best_full_success: | |
| best_full_success = result["full_success_rate"] | |
| best_result = result | |
| best_split = n_splits | |
| # Calculate metrics | |
| large_maker_value = bond_value(10, exp) | |
| small_maker_value = bond_value(1, exp) | |
| centralization_ratio = large_maker_value / small_maker_value | |
| consolidated = bond_value(10, exp) | |
| split = 10 * bond_value(1, exp) | |
| split_efficiency = (split / consolidated) * 100 | |
| if not best_result: | |
| print(f"Warning: No results for exponent {exp} in scenario {scenario.name}") | |
| continue | |
| results.append({ | |
| "exponent": exp, | |
| "full_sybil_rate": best_result["full_success_rate"], | |
| "avg_attacker_peers": best_result["avg_attacker_peers"], | |
| "majority_rate": best_result["attacker_majority_rate"], | |
| "optimal_splits": best_split, | |
| "centralization_ratio": centralization_ratio, | |
| "split_efficiency": split_efficiency, | |
| }) | |
| df = pd.DataFrame(results) | |
| df["marginal_improvement"] = -df["full_sybil_rate"].diff().fillna(0) | |
| return df | |
| def create_comparison_plot(scenarios_results: dict) -> go.Figure: | |
| """Create comparison visualization across scenarios""" | |
| fig = make_subplots( | |
| rows=3, cols=1, | |
| subplot_titles=( | |
| "Full Sybil Attack Success Rate", | |
| "Average Attacker-Controlled Peers", | |
| "Split Efficiency vs Centralization Tradeoff" | |
| ), | |
| specs=[[{"secondary_y": False}], [{"secondary_y": False}], [{"secondary_y": True}]], | |
| vertical_spacing=0.10 | |
| ) | |
| colors = ['#ef4444', '#3b82f6', '#10b981', '#f59e0b', '#8b5cf6'] | |
| for i, (name, df) in enumerate(scenarios_results.items()): | |
| # Full sybil success rate | |
| fig.add_trace( | |
| go.Scatter( | |
| x=df["exponent"], | |
| y=df["full_sybil_rate"], | |
| mode="lines+markers", | |
| name=name, | |
| line=dict(color=colors[i % len(colors)], width=2), | |
| legendgroup=name, | |
| ), | |
| row=1, col=1 | |
| ) | |
| # Average attacker peers | |
| fig.add_trace( | |
| go.Scatter( | |
| x=df["exponent"], | |
| y=df["avg_attacker_peers"], | |
| mode="lines+markers", | |
| name=name, | |
| line=dict(color=colors[i % len(colors)], width=2), | |
| legendgroup=name, | |
| showlegend=False, | |
| ), | |
| row=2, col=1 | |
| ) | |
| # Use first scenario for efficiency/centralization (same for all) | |
| first_df = list(scenarios_results.values())[0] | |
| fig.add_trace( | |
| go.Scatter( | |
| x=first_df["exponent"], | |
| y=first_df["split_efficiency"], | |
| mode="lines+markers", | |
| name="Split Efficiency", | |
| line=dict(color="#ec4899", width=2, dash='dash'), | |
| ), | |
| row=3, col=1, secondary_y=False | |
| ) | |
| fig.add_trace( | |
| go.Scatter( | |
| x=first_df["exponent"], | |
| y=first_df["centralization_ratio"], | |
| mode="lines+markers", | |
| name="Centralization", | |
| line=dict(color="#8b5cf6", width=2), | |
| ), | |
| row=3, col=1, secondary_y=True | |
| ) | |
| fig.update_xaxes(title_text="Exponent", row=1, col=1) | |
| fig.update_xaxes(title_text="Exponent", row=2, col=1) | |
| fig.update_xaxes(title_text="Exponent", row=3, col=1) | |
| fig.update_yaxes(title_text="Full Sybil Success (%)", row=1, col=1) | |
| fig.update_yaxes(title_text="Avg Attacker Peers", row=2, col=1) | |
| fig.update_yaxes(title_text="Split Efficiency (%)", row=3, col=1, secondary_y=False) | |
| fig.update_yaxes(title_text="Income Ratio (10 BTC / 1 BTC)", row=3, col=1, secondary_y=True) | |
| fig.update_layout( | |
| height=1100, | |
| title_text="Fidelity Bond Exponent Analysis: Real Orderbook Scenarios", | |
| hovermode='x unified' | |
| ) | |
| return fig | |
| if __name__ == "__main__": | |
| print("=" * 80) | |
| print("REAL JOINMARKET ORDERBOOK ANALYSIS") | |
| print("=" * 80) | |
| print(f"\nCurrent orderbook: {len(REAL_ORDERBOOK)} makers") | |
| print(f"Total locked: {sum(REAL_ORDERBOOK):.2f} BTC") | |
| print(f"Top 5 makers: {sum(REAL_ORDERBOOK[:5]):.2f} BTC ({sum(REAL_ORDERBOOK[:5])/sum(REAL_ORDERBOOK)*100:.1f}%)") | |
| print(f"Median locked: {sorted(REAL_ORDERBOOK)[len(REAL_ORDERBOOK)//2]:.4f} BTC") | |
| # Define realistic attack scenarios | |
| scenarios = { | |
| "Small attacker (10 BTC, 2 peers)": AttackScenario( | |
| name="Small attacker", | |
| honest_bonds=REAL_ORDERBOOK.copy(), | |
| attacker_coins=10.0, | |
| taker_peers=2, # Small coinjoins | |
| description="Modest attacker vs 2-peer selection" | |
| ), | |
| "Medium attacker (25 BTC, 3 peers)": AttackScenario( | |
| name="Medium attacker", | |
| honest_bonds=REAL_ORDERBOOK.copy(), | |
| attacker_coins=25.0, | |
| taker_peers=3, # Medium coinjoins | |
| description="Well-funded attacker vs 3-peer selection" | |
| ), | |
| "Large attacker (50 BTC, 4 peers)": AttackScenario( | |
| name="Large attacker", | |
| honest_bonds=REAL_ORDERBOOK.copy(), | |
| attacker_coins=50.0, | |
| taker_peers=4, # Larger coinjoins | |
| description="Major threat actor vs 4-peer selection" | |
| ), | |
| "Insider (top maker, 2 peers)": AttackScenario( | |
| name="Insider attack", | |
| honest_bonds=REAL_ORDERBOOK[1:], # Remove top maker | |
| attacker_coins=REAL_ORDERBOOK[0], # Top maker is attacker | |
| taker_peers=2, | |
| description="Top maker (30 BTC) turns malicious" | |
| ), | |
| "Whale attacker (100 BTC, 5 peers)": AttackScenario( | |
| name="Whale attacker", | |
| honest_bonds=REAL_ORDERBOOK.copy(), | |
| attacker_coins=100.0, | |
| taker_peers=5, | |
| description="Extreme threat: 100 BTC attacker" | |
| ), | |
| } | |
| exponents = [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.5, 3.0, 5.0, 10.0] | |
| all_results = {} | |
| for scenario_name, scenario in scenarios.items(): | |
| print(f"\n{'=' * 80}") | |
| print(f"SCENARIO: {scenario_name}") | |
| print(f"{'=' * 80}") | |
| print(f"Description: {scenario.description}") | |
| print(f"Attacker coins: {scenario.attacker_coins:.2f} BTC") | |
| print(f"Honest makers: {len(scenario.honest_bonds)}") | |
| print(f"Taker chooses: {scenario.taker_peers} peers") | |
| print(f"\nAnalyzing...") | |
| df = analyze_attack_scenario(scenario, exponents) | |
| all_results[scenario_name] = df | |
| print("\n" + df.to_string(index=False)) | |
| # Key insights - safely handle missing exponents | |
| print(f"\nKEY RESULTS:") | |
| for exp_val in [1.0, 1.3, 2.0, 3.0, 5.0, 10.0]: | |
| exp_data = df[df["exponent"] == exp_val] | |
| if not exp_data.empty: | |
| exp_row = exp_data.iloc[0] | |
| print(f" Exponent {exp_val}:") | |
| print(f" Full Sybil: {exp_row['full_sybil_rate']:.2f}% | Avg peers: {exp_row['avg_attacker_peers']:.2f} | Majority: {exp_row['majority_rate']:.1f}%") | |
| print(f" Optimal splits: {exp_row['optimal_splits']}") | |
| print(f" Centralization: {exp_row['centralization_ratio']:.1f}x (10 BTC / 1 BTC maker income)") | |
| exp_1_data = df[df["exponent"] == 1.0] | |
| exp_2_data = df[df["exponent"] == 2.0] | |
| # Compare tradeoffs between exponents | |
| print(f"\nTRADEOFF ANALYSIS:") | |
| comparisons = [(1.0, 1.3), (1.3, 2.0), (2.0, 3.0), (3.0, 5.0)] | |
| for exp_a, exp_b in comparisons: | |
| data_a = df[df["exponent"] == exp_a] | |
| data_b = df[df["exponent"] == exp_b] | |
| if not data_a.empty and not data_b.empty: | |
| a = data_a.iloc[0] | |
| b = data_b.iloc[0] | |
| sybil_improvement = a['full_sybil_rate'] - b['full_sybil_rate'] | |
| if a['full_sybil_rate'] > 0: | |
| sybil_improvement_pct = (sybil_improvement / a['full_sybil_rate']) * 100 | |
| else: | |
| sybil_improvement_pct = 0 | |
| centralization_increase = b['centralization_ratio'] / a['centralization_ratio'] | |
| # Efficiency: how much Sybil resistance per unit of centralization | |
| if centralization_increase > 1: | |
| efficiency = sybil_improvement_pct / (centralization_increase - 1) | |
| else: | |
| efficiency = 0 | |
| print(f" {exp_a} → {exp_b}:") | |
| print(f" Sybil resistance improves by {sybil_improvement:.3f}% ({sybil_improvement_pct:.1f}% relative)") | |
| print(f" Centralization increases {centralization_increase:.1f}x") | |
| print(f" Efficiency: {efficiency:.2f}% Sybil improvement per 1x centralization") | |
| print("\n" + "=" * 80) | |
| print("TRADEOFF ANALYSIS: Sybil Resistance vs Centralization") | |
| print("=" * 80) | |
| print("\nFor each scenario, we analyze: Is the Sybil resistance gain worth the centralization cost?") | |
| for scenario_name, df in all_results.items(): | |
| print(f"\n{scenario_name}:") | |
| # Compare key exponents | |
| comparisons = [ | |
| (1.0, 1.3, "Linear to JoinMarket Current"), | |
| (1.3, 2.0, "JoinMarket Current to Quadratic"), | |
| (2.0, 3.0, "Quadratic to Cubic"), | |
| ] | |
| for exp_a, exp_b, label in comparisons: | |
| data_a = df[df["exponent"] == exp_a] | |
| data_b = df[df["exponent"] == exp_b] | |
| if data_a.empty or data_b.empty: | |
| continue | |
| a = data_a.iloc[0] | |
| b = data_b.iloc[0] | |
| # Calculate improvements and costs | |
| sybil_improvement = a['full_sybil_rate'] - b['full_sybil_rate'] | |
| if a['full_sybil_rate'] > 0.01: | |
| sybil_improvement_pct = (sybil_improvement / a['full_sybil_rate']) * 100 | |
| else: | |
| sybil_improvement_pct = 0 | |
| centralization_ratio = b['centralization_ratio'] / a['centralization_ratio'] | |
| # Efficiency metric: Sybil improvement per unit centralization | |
| if centralization_ratio > 1.01: | |
| efficiency = sybil_improvement_pct / (centralization_ratio - 1) | |
| else: | |
| efficiency = 0 | |
| print(f" {label} ({exp_a} → {exp_b}):") | |
| print(f" Sybil attack drops: {a['full_sybil_rate']:.3f}% → {b['full_sybil_rate']:.3f}% ({sybil_improvement_pct:.1f}% improvement)") | |
| print(f" Centralization: {a['centralization_ratio']:.1f}x → {b['centralization_ratio']:.1f}x ({centralization_ratio:.1f}x worse)") | |
| if efficiency > 0: | |
| print(f" Tradeoff efficiency: {efficiency:.2f}% Sybil improvement per 1x centralization increase") | |
| # Make a recommendation | |
| if sybil_improvement < 0.05 and centralization_ratio > 2: | |
| print(f" ⚠️ POOR TRADEOFF: Tiny Sybil gain ({sybil_improvement:.3f}%) for large centralization ({centralization_ratio:.1f}x)") | |
| elif sybil_improvement > 1.0 and centralization_ratio < 3: | |
| print(f" ✓ GOOD TRADEOFF: Significant Sybil improvement with moderate centralization") | |
| elif efficiency > 50: | |
| print(f" ✓ EFFICIENT: High Sybil improvement per unit centralization") | |
| # Create visualization | |
| fig = create_comparison_plot(all_results) | |
| fig.write_html("exponent_analysis.html") | |
| print("\nVisualization saved to: exponent_analysis.html") |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The Exponent Choice
Experiment
Scenarios
The analysis tests five attack scenarios against real JoinMarket orderbook data (44 makers, 144.17 BTC total locked):
For each scenario, the script finds the optimal splitting strategy for the attacker (how to divide their coins across multiple maker identities) and measures the success rate of achieving a full Sybil attack (controlling all peers in the coinjoin).
Results
Script output
Charts
Representative Scenario Results
Medium attacker (25 BTC, 3 peers) - Most balanced scenario showing typical tradeoffs:
Efficiency Score = Sybil improvement % ÷ centralization increase factor
Key insight: Moving from 1.0→1.3 gives 54.8% efficiency (excellent), while 1.3→2.0 drops to 19.9% efficiency (2.7x worse), and going beyond 2.0 yields <5% efficiency (terrible tradeoff). The quadratic exponent (2.0) requires 5x more centralization than 1.3 for diminishing security returns.
Key findings across all scenarios
SYBIL RESISTANCE:
CENTRALIZATION PRESSURE:
THE CRITICAL QUESTION:
Is a 0.1-1% improvement in Sybil resistance worth:
RECOMMENDATION:
Based on efficiency metrics, x=1.3 appears to offer the best balance: