.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "api/gallery/channels/interoperability/plot_03_bridge_exploits.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code or to run this example in your browser via JupyterLite. .. rst-class:: sphx-glr-example-title .. _sphx_glr_api_gallery_channels_interoperability_plot_03_bridge_exploits.py: 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. .. GENERATED FROM PYTHON SOURCE LINES 25-33 .. code-block:: Python from math import comb import matplotlib.pyplot as plt import blockchainkit as bk from blockchainkit.contracts import World from blockchainkit.crypto import public_key .. GENERATED FROM PYTHON SOURCE LINES 34-36 Ronin: five keys of nine ------------------------ .. GENERATED FROM PYTHON SOURCE LINES 36-55 .. code-block:: Python 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 .. rst-class:: sphx-glr-script-out .. code-block:: none first successful theft with 5 keys .. GENERATED FROM PYTHON SOURCE LINES 56-58 Wormhole: a verifier chosen by the caller ----------------------------------------- .. GENERATED FROM PYTHON SOURCE LINES 58-89 .. code-block:: Python 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() .. image-sg:: /api/gallery/channels/interoperability/images/sphx_glr_plot_03_bridge_exploits_001.png :alt: Ronin: a 5-of-9 threshold that was really 1-of-1, Wormhole: forged verification :srcset: /api/gallery/channels/interoperability/images/sphx_glr_plot_03_bridge_exploits_001.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none drained before the fix: 1000 after: 0 .. GENERATED FROM PYTHON SOURCE LINES 90-96 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? .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 0.275 seconds) .. _sphx_glr_download_api_gallery_channels_interoperability_plot_03_bridge_exploits.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: lite-badge .. image:: images/jupyterlite_badge_logo.svg :target: ../../../../lite/lab/index.html?path=api/gallery/channels/interoperability/plot_03_bridge_exploits.ipynb :alt: Launch JupyterLite :width: 150 px .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: plot_03_bridge_exploits.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: plot_03_bridge_exploits.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: plot_03_bridge_exploits.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_