|
from __future__ import annotations |
|
|
|
import math |
|
from typing import Union |
|
|
|
import numpy as np |
|
import pandas as pd |
|
from scipy.optimize import brentq |
|
from scipy.stats import poisson |
|
|
|
|
|
def _attack_success_prob(q: float, z: int) -> float: |
|
""" |
|
Nakamoto-style probability that an attacker with hashrate fraction q |
|
(0 < q < 1) eventually catches up and can reverse a transaction that is z |
|
blocks deep (z >= 0). For q >= 0.5, the probability is 1 by majority control. |
|
|
|
Uses the standard Poisson approximation (Bitcoin whitepaper): |
|
P(q, z) = 1 - sum_{k=0}^{z} [ e^{-λ} λ^k / k! * (1 - (q/p)^{z-k}) ] |
|
where p = 1 - q and λ = z * q / p. |
|
|
|
Parameters |
|
---------- |
|
q : float |
|
Attacker/pool hashrate share as a fraction in (0, 1). |
|
z : int |
|
Confirmation depth (non-negative integer). |
|
|
|
Returns |
|
------- |
|
float |
|
Attack success probability P(q, z). |
|
""" |
|
if q >= 0.5: |
|
return 1.0 |
|
if z <= 0: |
|
# With 0 confirmations, success probability is trivially 1 |
|
# in the settlement sense (no finality yet). |
|
return 1.0 |
|
|
|
p = 1.0 - q |
|
lam = z * q / p |
|
|
|
# Vectorized evaluation of the Poisson PMF for k = 0..z |
|
ks = np.arange(z + 1) |
|
pmf = poisson.pmf(ks, lam) |
|
|
|
# Factor (1 - (q/p)^{z-k}); handle numerical stability when q/p ~ 1 |
|
ratio = q / p |
|
# pow_term[j] = ratio**(z - k_j) |
|
pow_term = np.power(ratio, z - ks, dtype=float) |
|
terms = pmf * (1.0 - pow_term) |
|
|
|
s = float(np.sum(terms)) |
|
return max(0.0, min(1.0, 1.0 - s)) |
|
|
|
|
|
def required_confirmations( |
|
pool_share_pct: float, |
|
epsilon: float = 1e-6, |
|
z_max: int = 1_000_000 |
|
) -> Union[int, float]: |
|
""" |
|
Given the largest observed pool share (in percent), return the warranted |
|
number of confirmations z such that the double-spend success probability |
|
P(q, z) <= epsilon under the Poisson model. |
|
|
|
If the input percentage is >= 50%, majority control is possible; no finite z |
|
can guarantee safety, so the function returns math.inf. |
|
|
|
Parameters |
|
---------- |
|
pool_share_pct : float |
|
Largest pool share as a percentage, e.g. 55.0 for 55%. |
|
epsilon : float, optional |
|
Target upper bound on attack success probability, default 1e-6. |
|
z_max : int, optional |
|
Hard cap to prevent runaway search in extreme edge cases. |
|
|
|
Returns |
|
------- |
|
int | float |
|
Minimal integer confirmations z meeting the risk target, or math.inf if |
|
pool_share_pct >= 50. |
|
""" |
|
q = pool_share_pct / 100.0 |
|
if q >= 0.5: |
|
return math.inf |
|
if q <= 0.0: |
|
return 0 |
|
|
|
# Exponential search to find an upper bound z_hi with P(q, z_hi) <= epsilon |
|
z_lo = 0 |
|
z_hi = 1 |
|
while z_hi <= z_max and _attack_success_prob(q, z_hi) > epsilon: |
|
z_lo = z_hi |
|
z_hi <<= 1 # double |
|
|
|
if z_hi > z_max: |
|
# Could not meet epsilon within bound; return the cap (conservative). |
|
return z_max |
|
|
|
# Binary search for the minimal z in (z_lo, z_hi] with P(q, z) <= epsilon |
|
while z_lo + 1 < z_hi: |
|
z_mid = (z_lo + z_hi) // 2 |
|
if _attack_success_prob(q, z_mid) <= epsilon: |
|
z_hi = z_mid |
|
else: |
|
z_lo = z_mid |
|
return z_hi |
|
|
|
|
|
def max_pool_share_for_confirmations( |
|
z: int, |
|
epsilon: float = 1e-6 |
|
) -> float: |
|
""" |
|
Given a proposed confirmation count z, return the largest pool share |
|
(as a percentage) such that P(q, z) <= epsilon. This is found by solving |
|
P(q, z) = epsilon for q in (0, 0.5) using Brent's method. |
|
|
|
Parameters |
|
---------- |
|
z : int |
|
Proposed confirmation depth (non-negative integer). |
|
epsilon : float, optional |
|
Target upper bound on attack success probability, default 1e-6. |
|
|
|
Returns |
|
------- |
|
float |
|
Largest pool share in percent (0 <= result < 50) satisfying the risk |
|
target. If even an infinitesimal attacker exceeds epsilon at this z, |
|
returns 0.0. If epsilon is met arbitrary close to 50%, returns a value |
|
extremely close to but below 50.0. |
|
""" |
|
if z <= 0: |
|
return 0.0 # With z=0, only q≈0 would satisfy small epsilon. |
|
|
|
# Define f(q) = P(q, z) - epsilon; we seek the largest q with f(q) <= 0. |
|
def f(q: float) -> float: |
|
return _attack_success_prob(q, z) - epsilon |
|
|
|
q_low = 1e-12 |
|
q_high = 0.5 - 1e-12 |
|
|
|
# If even q_low violates the risk target, the answer is effectively 0% |
|
if f(q_low) > 0.0: |
|
return 0.0 |
|
|
|
# If the target is still satisfied arbitrarily close to 50%, return ~50% |
|
if f(q_high) <= 0.0: |
|
return 50.0 - 1e-10 |
|
|
|
# Root where P(q, z) = epsilon. This gives the *threshold* q*; |
|
# any q <= q* satisfies the target. |
|
q_star = brentq(f, q_low, q_high, maxiter=200, xtol=1e-12, rtol=1e-10) |
|
return 100.0 * q_star |
|
|
|
|
|
def generate_confirmations_table() -> str: |
|
""" |
|
Generate a raw markdown table of required confirmations |
|
for pool shares from 0% to 50%, with ε=1e-6 and ε=1e-3. |
|
|
|
Returns |
|
------- |
|
str |
|
A markdown-formatted table as a string. |
|
""" |
|
pool_shares = list(range(0, 51)) |
|
epsilon1 = 1e-6 |
|
epsilon2 = 1e-3 |
|
|
|
results = [] |
|
for ps in pool_shares: |
|
z1 = required_confirmations(ps, epsilon=epsilon1) |
|
z2 = required_confirmations(ps, epsilon=epsilon2) |
|
results.append((ps, z1, z2)) |
|
|
|
df = pd.DataFrame(results, columns=[ |
|
"Pool Share %", |
|
"Required Confirmations (ε=1e-6)", |
|
"Required Confirmations (ε=1e-3)" |
|
]) |
|
|
|
return df.to_markdown(index=False) |
|
|
|
if __name__ == "__main__": |
|
print(generate_confirmations_table()) |