Postavit blockchain je snadné. To zajímavé začne, když se ho zkusíš rozbít, protože každý neúspěšný útok ti řekne, proč je ta konstrukce taková, jaká je.
Tři útoky, všechny běží za sekundy, a po nich už ti nikdo nemusí vysvětlovat, proč se čeká na potvrzení.
▶ Spustitelné. Ulož jako
utoky.pydo stejné složky jakochain.pyz minulé kapitoly a pusťpython3 utoky.py.
Celý program
"""Tři útoky na vlastní blockchain. Vyžaduje chain.py ve stejné složce."""
import copy, time
from chain import Blok, vymin, platny, postav, merkle, OBTIZNOST
txs = lambda i: [f'tx: Alice -> Bob, {i+1} coinu', f'tx: odmena mineru za blok {i}']
print('═══ ÚTOK 1: PŘEPIŠ TRANSAKCI V BLOKU 2 ═══')
chain, _ = postav(5, txs)
utok = copy.deepcopy(chain)
utok[2].txs = ['tx: Alice -> Eva, 1000 coinu', 'tx: odmena mineru za blok 2']
print(f' po přepsání: {platny(utok)}')
print(f' blok 2 má teď hash {utok[2].hash[:12]}, ale blok 3 odkazuje na {utok[3].predchozi[:12]}')
print(' Změna jedné transakce rozbila VŠECHNY následující bloky.\n')
print(' Kolik práce stojí to zamaskovat? Musíš přemínovat blok 2 i všechny za ním:')
t0, prace_utok, prev = time.time(), 0, utok[1].hash
for i in range(2, 5):
b, p = vymin(i, prev, utok[i].txs)
utok[i] = b; prev = b.hash; prace_utok += p
print(f' {prace_utok:,} pokusů za {time.time()-t0:.2f} s -> {platny(utok)}')
print(' A to byly tři bloky. Čím hlubší transakce, tím dražší přepis.')
print('\n═══ ÚTOK 2: FORK A REORG ═══')
zaklad, _ = postav(3, txs)
vetev_A, praceA = postav(2, lambda i: [f'tx: A verze bloku {i+3}'], zaklad[-1].hash)
vetev_B, praceB = postav(3, lambda i: [f'tx: B verze bloku {i+3}'], zaklad[-1].hash)
print(f' společný základ: {len(zaklad)} bloky')
print(f' větev A: +{len(vetev_A)} bloky ({praceA:,} pokusů)')
print(f' větev B: +{len(vetev_B)} bloky ({praceB:,} pokusů)')
vyhrava = 'B' if len(vetev_B) > len(vetev_A) else 'A'
prohrava = 'A' if vyhrava == 'B' else 'B'
print(f' Pravidlo: vyhrává nejdelší řetěz. Vyhrává {vyhrava}.')
print(f' Transakce z větve {prohrava} se vrací do fronty, jako by nikdy nebyly.')
print(' Všimni si, že vítěz nemusel udělat víc práce. Měl jen štěstí.')
print('\n═══ ÚTOK 3: DVOJÍ UTRACENÍ ═══')
print(' Alice zaplatí Bobovi a zároveň tajně mine řetěz, kde ty samé coiny pošle sobě.')
print(' Nestačí jí dohnat pár bloků. Musí síť PŘEDBĚHNOUT, a to závisí na tom,')
print(' jak velký podíl výpočetní síly ovládá.\n')
def sance(q, z):
"""Šance, že útočník s podílem q dohoní z bloků. Klasická úloha o ruinovaném hráči."""
return 1.0 if q >= 0.5 else (q / (1 - q)) ** z
print(f" {'potvrzení':>10} | {'útočník 10 %':>13} | {'útočník 30 %':>13} | {'útočník 45 %':>13}")
print(' ' + '-' * 60)
for z in (0, 1, 2, 3, 6, 10):
r = [f'{sance(q,z):.2e}' if sance(q,z) < 0.001 else f'{sance(q,z):.1%}' for q in (0.10, 0.30, 0.45)]
print(f' {z:>10} | {r[0]:>13} | {r[1]:>13} | {r[2]:>13}')
print('\n Šest potvrzení srazí útočníka s 10 % síly na jednu z půl milionu.')
print(' Útočník s 45 % má ale i po šesti potvrzeních přes 30 %, a nad 50 % uspěje vždy.')
Útok 1: přepiš historii
po přepsání: (False, 'blok 2 nemá práci')
blok 2 má teď hash 59dac5d83158, ale blok 3 odkazuje na 00008aaaf996
Změna jedné transakce rozbila VŠECHNY následující bloky.
Kolik práce stojí to zamaskovat? Musíš přemínovat blok 2 i všechny za ním:
214,885 pokusů za 0.41 s -> (True, 'ok')
Přepsání jedné transakce nešlo ututlat. Změnil se Merkle koren, tím hash bloku, a tím se rozbil odkaz v následujícím bloku. Zamaskovat to znamená přemínovat všechno od změny dál.
U tří bloků to trvalo 0,41 sekundy. To je zároveň to, co je na téhle konstrukci geniální a nudné zároveň: cena přepisu roste s hloubkou lineárně a nedá se obejít. Ne šifrováním, ne oprávněními, jen prací.
Útok 2: fork a reorg
společný základ: 3 bloky
větev A: +2 bloky (179,403 pokusů)
větev B: +3 bloky (68,191 pokusů)
Pravidlo: vyhrává nejdelší řetěz. Vyhrává B.
Tady je nejzajímavější číslo celé kapitoly a je snadné ho přehlédnout: větev B vyhrála, přestože udělala méně než poloviční práci. 68 tisíc pokusů proti 179 tisícům, a přece má o blok víc.
Měla štěstí. Hledání hashe je losování, takže krátkodobě může menší miner předběhnout většího. Z toho plynou dvě věci:
- Nic není nikdy definitivní. Vždycky může přijít delší řetěz a tvůj blok z historie vypadne. Tomu se říká reorg a transakce z prohrané větve se vrátí do fronty.
- Dlouhodobě rozhoduje výkon, krátkodobě náhoda. Proto se u malých částek čeká jedno potvrzení a u velkých šest, a proto se pravidlo nejmenuje „vyhrává první", ale „vyhrává nejdelší".
Útok 3: dvojí utracení
A teď to, kvůli čemu celý blockchain existuje. Alice zaplatí Bobovi, Bob odešle zboží, a Alice mezitím tajně mine alternativní řetěz, ve kterém ty samé coiny poslala sobě. Když svůj řetěz zveřejní a je delší, Bobova platba zmizí.
Nestačí jí ale dohnat pár bloků. Musí síť předběhnout, a to závisí na jejím podílu výkonu:
potvrzení | útočník 10 % | útočník 30 % | útočník 45 %
------------------------------------------------------------
0 | 100.0% | 100.0% | 100.0%
1 | 11.1% | 42.9% | 81.8%
2 | 1.2% | 18.4% | 66.9%
3 | 0.1% | 7.9% | 54.8%
6 | 1.88e-06 | 0.6% | 30.0%
10 | 2.87e-10 | 2.09e-04 | 13.4%
Čti si v té tabulce tři věci:
- Nula potvrzení znamená nulovou ochranu. Útočník uspěje vždy, protože nemusel nic dohánět. Proto nikdo neposílá zboží proti nepotvrzené transakci.
- Šest potvrzení je dobré číslo, ale ne magické. Proti útočníkovi s desetinou výkonu je to šance jedna k půl milionu. Proti útočníkovi s 45 % je to pořád přes třicet procent.
- Nad polovinou výkonu uspěje útočník vždycky, jen si musí počkat. Není to hranice, za kterou se něco rozbije, je to hranice, za kterou přestane platit statistika.
Odtud plyne, proč je koncentrace mineru u malých sítí skutečný problém, a ne teoretický: u sítě, kde si hodinu výkonu koupíš za pár tisíc dolarů, není 51 % otázka schopnosti, ale rozpočtu.
Co ty útoky nedokázaly
Stejně důležité je, co útočník nemůže, i kdyby měl výkonu kolik chce:
| Nemůže | Protože |
|---|---|
| Ukrást coiny z cizí adresy | potřeboval by privátní klíč, mining s tím nepomůže |
| Vyrobit coiny z ničeho | pravidla emise ověřuje každý uzel nezávisle |
| Změnit blok, na kterém stojí tisíc dalších | přemínování by stálo víc než celá síť za roky |
| Zabránit tomu, aby jeho útok byl vidět | reorg je pro každého uzel viditelný |
Útok 51 % tedy neznamená „ovládl blockchain". Znamená „může cenzurovat transakce a přepsat posledních několik bloků". To je dost na dvojí utracení a málo na krádež.
Cvičení
- Uprav útok 1 tak, aby přepsal blok 0 místo bloku 2. O kolik víc práce to bude a proč?
- V útoku 2 vyhrála větev s menší prací. Napiš, jak bys pravidlo „nejdelší řetěz" změnil, aby vyhrávala větev s větší prací, a proč to tak Bitcoin nedělá u počtu bloků.
- Spočítej, kolik potvrzení bys chtěl u platby, kde ti hrozí ztráta milionu, proti útočníkovi s 30 % výkonu, když chceš riziko pod jednu promile.
Náčrt řešení: rozbal, až si cvičení zkusíš sám
- Zhruba o dva bloky práce víc, protože musíš přemínovat pět bloků místo tří. Cena roste lineárně s hloubkou, ne exponenciálně, což je dobré vědět. Exponenciální je až ta část, kdy musíš dohánět síť, která mezitím mine dál. Odtud plyne, že ochrana staré transakce není v tom, že by přepis byl matematicky nemožný, ale v tom, že za tu dobu naroste tolik práce, že se to nedá dohnat.
- Bitcoin skutečně nepočítá bloky, ale nasčítanou obtížnost. Pravidlo se často zjednodušuje na „nejdelší řetěz", ale správně je „řetěz s největší kumulativní prací". Rozdíl se projeví, když se obtížnost mezi větvemi liší: kratší řetěz s vyšší obtížností může vyhrát. V našem modelu je obtížnost konstantní, takže počet bloků a práce jsou totéž, a proto tam ten rozdíl není vidět. Ve skutečné síti by útočník s uměle sníženou obtížností jinak vyrobil dlouhý levný řetěz.
- Šest potvrzení. Z tabulky: útočník s 30 % má po šesti potvrzeních 0,6 %, což je pořád nad
promile. Po sedmi je to
(0,4286)⁷ ≈ 0,26 %, po devíti≈ 0,05 %. Chceš li pod promili, potřebuješ osm až devět potvrzení. Praktický závěr: počet potvrzení se má odvíjet od hodnoty transakce a od toho, jak koncentrovaný je mining v dané síti, ne od zvyku.
Shrnutí
- Přepsání transakce rozbije všechny následující bloky. Zamaskovat to jde jen přemínováním.
- Cena přepisu roste s hloubkou lineárně, ale dohánění běžící sítě je exponenciálně nevýhodné.
- Fork vyhrává delší řetěz, ne ten s větší prací nebo ten první. Krátkodobě rozhoduje náhoda.
- Nula potvrzení nedává žádnou ochranu. Šest je dobré proti malému útočníkovi, ne proti velkému.
- Útok 51 % umí cenzuru a dvojí utracení, neumí krádež ani emisi z ničeho.
Proč nejde v blockchainu přepsat jednu starou transakci?
Protože změna transakce změní Merkle koren bloku, tím hash bloku, a tím se rozbije odkaz v následujícím bloku i ve všech dalších. Aby to útočník zamaskoval, musí přemínovat každý blok od změny dál, a přitom dohánět síť, která pokračuje. Cena tedy roste s hloubkou transakce.
Vyhrává při forku větev, která udělala víc práce?
Ve zjednodušeném modelu s konstantní obtížností vyhrává delší větev, a ta nemusela udělat víc práce, protože hledání hashe je losování. Skutečná pravidla proto porovnávají nasčítanou obtížnost, ne počet bloků, aby útočník nemohl vyrobit dlouhý řetěz s umělé nízkou obtížností.
Co útočník s víc než polovinou výpočetní síly nemůže?
Nemůže utrácet z cizích adres, protože k tomu by potřeboval privátní klíče, a nemůže vyrobit coiny mimo pravidla emise, protože to ověřuje každý uzel nezávisle. Může cenzurovat transakce a přepsat posledních několik bloků, což stačí na dvojí utracení. Krádež to není.
