r"""
Bridge failures: the Ronin and Wormhole exploits (2022)
=======================================================

Most bridges do not verify the other chain; they trust a committee to
sign withdrawals, and are exactly as safe as its keys and the code that
checks its signatures. In 2022 both failed. Ronin released funds on 5 of
9 validator signatures; one company ran four validators and still held
permission to sign for a fifth, so a single breach yielded 173,600 ether
and 25.5 million USDC. Wormhole's Solana program trusted an account,
supplied by the caller, saying that the guardians' signatures had been
checked; a forged account minted 120,000 wrapped ether from nothing.

For a threshold of ``t`` of ``n`` keys held by independent parties, each
compromised with probability ``p``, the bridge falls with probability

.. math::

   \sum_{k=t}^{n} \binom{n}{k} p^k (1-p)^{n-k},

but the keys were not independent: four of Ronin's five sat with one party.
"""

# %%
from math import comb

import matplotlib.pyplot as plt

import blockchainkit as bk
from blockchainkit.contracts import World
from blockchainkit.crypto import public_key

# %%
# Ronin: five keys of nine
# ------------------------

world = World()
world.fund("users", 1_000)
validators = [public_key(k) for k in range(1, 10)]
ronin = world.deploy("sky mavis", bk.channels.ValidatorBridge, validators, 5, name="Ronin")
world.transact("users", ronin, "deposit", value=1_000)

stolen_with = {}
for keys in range(1, 10):
    approvals = [
        bk.channels.sign_withdrawal(k, ronin, "attacker", 1_000, keys) for k in range(1, keys + 1)
    ]
    receipt = world.transact("attacker", ronin, "withdraw", "attacker", 1_000, keys, approvals)
    stolen_with[keys] = receipt.success
    if receipt.success:
        break
print("first successful theft with", max(stolen_with), "keys")
assert max(stolen_with) == 5 and world.balance("attacker") == 1_000

# %%
# Wormhole: a verifier chosen by the caller
# -----------------------------------------

drained = {}
for fixed in (False, True):
    world = World()
    world.fund("users", 1_000)
    guardians = [public_key(k) for k in range(1, 20)]
    verifier = world.deploy("wormhole", bk.channels.GuardianVerifier, guardians, 13)
    bridge = world.deploy("wormhole", bk.channels.WormholeBridge, verifier, fixed)
    world.transact("users", bridge, "deposit", value=1_000)
    forged = world.deploy("attacker", bk.channels.ForgedVerifier)
    world.transact("attacker", bridge, "complete_transfer", "attacker", 1_000, 0, forged)
    world.transact("attacker", bridge, "redeem", 1_000)
    drained[fixed] = world.balance("attacker")
print("drained before the fix:", drained[False], " after:", drained[True])
assert drained == {False: 1_000, True: 0}

fig, (left, right) = plt.subplots(1, 2, figsize=(11, 4))
p = [i / 100 for i in range(0, 101, 2)]
independent = [sum(comb(9, k) * q**k * (1 - q) ** (9 - k) for k in range(5, 10)) for q in p]
one_party = p  # Five keys behind one company's systems fall together.
left.plot(p, independent, color="#16a34a", label="9 independent validators")
left.plot(p, one_party, color="#dc2626", label="5 keys at one company")
left.set(xlabel="chance a party is compromised", ylabel="chance the bridge falls")
left.set_title("Ronin: a 5-of-9 threshold that was really 1-of-1")
left.legend()
right.bar(["vulnerable", "fixed"], [drained[False], drained[True]], color=["#dc2626", "#16a34a"])
right.set(ylabel="ether drained of 1,000 locked", title="Wormhole: forged verification")
fig.tight_layout()

plt.show()

# %%
# Exercise
# --------
# Change the Ronin bridge so that each organization's keys count once,
# whatever number of validators it runs. How many organizations must the
# attacker now compromise, and what does that suggest about counting
# signatures rather than signers?
