Postav si blockchain

Sto padesát řádků Pythonu, běží za sekundu.

Co se naučíš: Postavíš funkční blockchain v padesáti řádcích, který doběhne za půl sekundy.

10 min čtení + cvičeníNavazuje na:🌳 Merkle tree⛏️ Proof of work

Máš hash, podpisy a Merkle tree. Zbývá je slepit dohromady a je z toho blockchain. Celý se vejde do padesáti řádků a doběhne za necelou sekundu.

Tahle kapitola staví. Ta následující se ho pokusí podvrhnout, a tam se to teprve začne vyplácet.


▶ Spustitelné. Ulož jako chain.py a pusť python3 chain.py. Příští kapitola z tohohle souboru vychází, tak si ho nech.


Co je vlastně blok

Tři věci, nic víc:

Odkaz na hash předchůdce je to, co dělá z bloků řetěz. A protože hash závisí na celém obsahu bloku včetně toho odkazu, je každý blok zapečetěný do všech následujících. Odtud pochází ta neměnnost, o které se mluví: není to vlastnost úložiště, je to důsledek hashování.

Nonce je jediné číslo, které v bloku nic neznamená. Je tam jen proto, aby se dalo měnit, dokud hash nevyjde s dost nulami.


Celý blockchain

"""Blockchain od nuly. Ulož jako chain.py, budeme z něj dál vycházet."""
import hashlib, time

OBTIZNOST = 4          # kolik nul na začátku hashe. Každá další je 16x dražší.

def h(x):
    return hashlib.sha256(x.encode()).hexdigest()

def merkle(txs):
    if not txs: return h('')
    u = [h(t) for t in txs]
    while len(u) > 1:
        if len(u) % 2: u.append(u[-1])
        u = [h(u[i] + u[i+1]) for i in range(0, len(u), 2)]
    return u[0]

class Blok:
    def __init__(self, index, predchozi, txs, nonce=0):
        self.index, self.predchozi, self.txs, self.nonce = index, predchozi, txs, nonce

    @property
    def hash(self):
        # Hlavička bloku. Změna čehokoli v ní změní hash celý.
        return h(f'{self.index}|{self.predchozi}|{merkle(self.txs)}|{self.nonce}')

    def __repr__(self):
        return f'blok {self.index} hash {self.hash[:12]} nonce {self.nonce}'

def vymin(index, predchozi, txs, obtiznost=OBTIZNOST):
    """Zkoušej nonce, dokud hash nezačíná daným počtem nul. Nic jiného mining není."""
    b = Blok(index, predchozi, txs)
    cil, pokusy = '0' * obtiznost, 0
    while not b.hash.startswith(cil):
        b.nonce += 1; pokusy += 1
    return b, pokusy

def platny(chain, obtiznost=OBTIZNOST):
    """Řetěz je platný, když každý blok má práci a odkazuje na hash předchůdce."""
    for i, b in enumerate(chain):
        if not b.hash.startswith('0' * obtiznost):
            return False, f'blok {i} nemá práci'
        if i and b.predchozi != chain[i - 1].hash:
            return False, f'blok {i} neodkazuje na předchůdce'
    return True, 'ok'

def postav(pocet, txs_fn, start_hash='0' * 64, obtiznost=OBTIZNOST):
    chain, prev, prace = [], start_hash, 0
    for i in range(pocet):
        b, p = vymin(i, prev, txs_fn(i), obtiznost)
        chain.append(b); prev = b.hash; prace += p
    return chain, prace

if __name__ == '__main__':
    txs = lambda i: [f'tx: Alice -> Bob, {i+1} coinu', f'tx: odmena mineru za blok {i}']
    t0 = time.time()
    chain, prace = postav(5, txs)
    for b in chain: print(f'  {b}')
    print(f'\n  celkem {prace:,} pokusů za {time.time()-t0:.2f} s')
    print(f'  platnost: {platny(chain)}')
    print(f'\n  Merkle koren bloku 0: {merkle(chain[0].txs)[:32]}...')

Co z toho vyšlo

  blok 0 hash 0000171f4dc1 nonce 91278
  blok 1 hash 0000e63277bf nonce 9949
  blok 2 hash 00008aaaf996 nonce 27705
  blok 3 hash 0000863354a3 nonce 37864
  blok 4 hash 00005d50d82d nonce 31362

  celkem 198,158 pokusů za 0.38 s
  platnost: (True, 'ok')

Pět bloků, dvě stě tisíc pokusů, necelá půlsekunda. A je to funkční blockchain: má práci, má řetězení, má Merkle koreny a jde ověřit.

Podívej se na hodnoty nonce: 91 278, potom 9 949, potom 27 705. Skáčou o řád, protože hledání je hra na náhodu. Očekávaná hodnota je při čtyřech nulách 16⁴ = 65 536, ale jednotlivé bloky se od ní klidně liší pětinásobně v obou směrech. Přesně proto se v Bitcoinu bloky neobjevují každých deset minut, ale v průměru každých deset minut.


Co v tom schválně není

Aby se to dalo přečíst na jeden zátah, chybí tomu tři věci, které skutečná síť má:

ChybíCo by to přidalo
Podpisy u transakcíteď je transakce jen text, kdokoli by mohl napsat cokoli
Kontrola zůstatkůnikdo neověřuje, že Alice ty coiny má
Síť a šíření blokůběží to v jednom procesu, žádná komunikace

První dvě si můžeš doplnit: podpisy máš z kapitoly o klíčích, kontrola zůstatků je slovník a odečítání. Síť je téma samostatné kapitoly.

Podstatné je, že tyhle tři chybějící věci nemají nic společného s tím, co dělá blockchain blockchainem. Konsensus na pořadí funguje i bez nich.


Obtížnost si můžeš pohladit

Zkus změnit OBTIZNOST na 5 a spustit to znovu. Doba běhu vyskočí zhruba šestnáctkrát. Na 6 už si počkáš minuty. To je celá regulace, kterou skutečná síť používá k tomu, aby držela konstantní odstup mezi bloky, i když se výpočetní výkon změní tisícinásobně.


Cvičení

  1. Doplň do bloku časovou značku. Co se stane s hashem a proč to musíš udělat před mining, ne po.
  2. Přidej ke transakcím podpisy z kapitoly o klíčích a kontrolu, že podpis odpovídá odesílateli.
  3. Změň OBTIZNOST na 5 a změř dobu. Kolikrát to bylo pomalejší a odpovídá to očekávání?
Náčrt řešení: rozbal, až si cvičení zkusíš sám
  1. Časová značka musí být součástí hlavičky, tedy vstupu do hashe, jinak by ji šlo dodatečně změnit. A protože je součástí hashe, musí být nastavená před mining: kdybys ji dopsal potom, změní se hash a přestane mít potřebné nuly. To je zároveň odpověď na otázku, proč miner nemůže „datovat blok zpětně" bez přemínování. Ve skutečných sítích má značka jen omezenou volnost, protože se ověřuje proti mediánu předchozích bloků a proti času uzlů.
  2. Transakce se změní z textu na strukturu s odesílatelem, příjemcem, částkou a podpisem, a do platny přidáš kontrolu, že podpis nad hashem transakce ověří veřejný klíč odesílatele. Klíčové je, co se podepisuje: musí to být celá transakce včetně částky a příjemce, jinak by šlo podpis přenést na jinou platbu. Zůstatky pak spočítáš průchodem řetězu od začátku.
  3. Zhruba šestnáctkrát, a odpovídá. Očekávaný počet pokusů je 16⁵ = 1 048 576 proti 16⁴ = 65 536. Naměříš rozptyl, protože je to jediný pokus na blok, ale při pěti blocích se to zprůměruje dost na to, aby byl faktor rozpoznatelný. Pokud ti vyjde výrazně jinak, spusť to znovu: jeden běh je málo.

Shrnutí

  • Blok obsahuje hash předchůdce, Merkle koren transakcí a nonce. Nic víc není potřeba.
  • Neměnnost není vlastnost úložiště, ale důsledek toho, že hash bloku závisí na hashi předchůdce.
  • Nonce nemá význam, existuje jen proto, aby se dalo hledat.
  • Funkční blockchain má padesát řádků a běží za půl sekundy.
  • Bloky se objevují v průměru po dané době, jednotlivé odstupy silně kolísají.
Co konkrétně dělá blockchain neměnným?

Hash každého bloku se počítá i z hashe předchůdce, takže změna jakéhokoli staršího bloku změní jeho hash, čímž se rozbije odkaz v následujícím bloku a kaskádovitě ve všech dalších. Neměnnost tedy není vlastnost databáze, ale důsledek řetězení hashů. Kdo chce něco přepsat, musí přemínovat všechno od té změny dál.

K čemu je v bloku nonce?

Je to jediné číslo v hlavičce, které nenese žádnou informaci. Existuje proto, aby ho miner mohl libovolně měnit a hledat takovou hodnotu, při které hash celého bloku začíná požadovaným počtem nul. Bez nonce by nebylo co měnit, protože ostatní položky hlavičky jsou dané obsahem bloku.

Proč se bloky neobjevují přesně po deseti minutách?

Protože hledání hashe je náhodný proces. Očekávaný počet pokusů odpovídá obtížnosti, ale konkrétní blok se najde třeba pětkrát rychleji nebo pětkrát pomaleji. Obtížnost proto reguluje pouze průměrný odstup, jednotlivé mezery mezi bloky kolísají a je to normální.