Vytvárame vlastný blockchain s vlastnou kryptomenou (from scratch)

Popularita technológie blockchain stúpa nielen u špekulantov, ale už aj medzi programátormi a IT firmami, ktoré dostávajú prvé zákazky od klientov na vytvorenie vlastných blockchainov. Stretol som sa s nejednou otázkou ako vytvoriť vlastný blockchain, vlastnú kryptomenu a blablabla. Moja prvá otázka bola ale vždy “Prečo?”. Pokiaľ je projekt len nejaká fancy appka, prečo by potrebovala vlastný blockchain, keď jednoducho sa dá použiť Ethereum a hotovo. Pod slovom “hotovo” rozumej XY výhod aké prináša sieť s už zabehnutou infraštruktúrou, podporou veľkej komunity a pravidelnými security updatemi. Samozrejme, ak ideme stavať vlastný systém pre dodávateľský reťazec alebo ideme disruptnúť poisťovníctvo, nezaobídeme sa bez vlastného blockchainu či už bude public alebo private. Dnes sa pozrieme na to, ako taký blockchain funguje z pohľadu kódu. Samozrejme ide len o čistý príklad implementovaný v jazyku Javascript.

Pochopenie toho ako blockchain funguje nieje jednoduché. Kým som to sám dostal do hlavy, prezrel som tony videí a prečítal kvantá článkov. Trvalo chvíľu, kým som túto technológiu konečne pochopil tak, že som o nej napísal článok kde som sa ju snažil vysvetliť čo najjednoduchšie ako sa len dá. To bola ale iba teória. Dnes budem písať znova o tom ako blockchain funguje, ale spoločne sa budeme snažiť pochopiť jeho technológie z pohľadu kódu. Celý kód je samozrejme commitnutý na mojom githube -> tu. Pokiaľ ale ešte nemáte zmáknutú teóriu, odporúčam najprv prejsť ako funguje blockchain -> tu.

Čo je to Blockchain alebo ako funguje technológia budúcnosti

Ešte pred tým než začneme, treba chápať blockchain z teoretického hľadiska, najmä jeho základné princípy. Blockchain je nemenný, sekvenčný reťazec informácií uložených v blokoch. Tie obsahujú informácie o transakciách alebo akékoľvek informácie chcete. Tieto bloky informácií sú zároveň spolu prepojené hash kódmi, ktoré fungujú ako kontrolný mechanizmus, alebo ako som to popisoval v článku, pečať pre jednotlivé bloky, ktoré ostanú už navždy nezmenené.

Vytvárame model nášho blockchainu

Vytvorili sme si triedu blockchain, ktorej konštruktor vytvorí počiatočný prázdny zoznam transakcií, resp. dát. Za manažovanie reťazca blokov zodpovedá táto trieda Blockchain. Ukladá transakcie a má niekoľko pomocných metód na pridávanie nových blokov do reťazca.

/**
 * Blockchain implementation
 * 
 * @class Blockchain
 */
class Blockchain {
  constructor () {
    if (chain.length === 0) {
      chain.push(
        BlockFactory(
          chain.length + 1,
          [],
          100,
          1
        ))
    }
  }

Ako vyzerá taký blok?

Každý blok v reťazci má svoj index, timestamp v UNIX formáte, zoznam transakcií alebo dát a Proof of Work číslo.

block = { 
 'index': 1, 
 'timestamp': 1506057125.900785, 
 'transactions': [ 
  { 
   'sender': "8527147fe1f5426f9dd545de4b27ee00", 
   'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f", 
   'amount': 5, 
  } 
 ], 
   'proof': 324984774000, 
   'previous_hash':  "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}

V tomto momente by sa nám mala vyjasňovať idea toho, ako blockchain funguje. Každý nový blok obsahuje hash kód toho predošlého, podľa ktorého sa vypočíta hash kód nového bloku. Práve tento mechanizmus zapečatenia každého bloku robí túto dátovú štruktúru memennou a neovplyvniteľnou. Na príklad, ak útočník pozmení staršie bloky, všetky bloky vytvorené po ňom budú obsahovať nesprávny hash, čím sa stávajú informácie nedôveryhodné.

Pridávanie transakcií do blokov

Na to, aby sme transakciu uložili do bloku, potrebujeme vytvoriť jej štruktúru so všetkými potrebnými dátami. Na to nám slúži praobyčajný objekt pushnutý do predpripraveného poľa.

/**
   * Adds transaction
   * 
   * @param {any} sender    Sender
   * @param {any} recipient Recipient
   * @param {any} value     Value
   * @memberof Blockchain
   */
  addTranstacion (sender, recipient, value) {
    currentTransactions.push({
      sender,
      recipient,
      value
    })
  }

Vytváranie blokov

Každá nová inštancia blockchainu potrebuje prvý blok v reťazci. Prvý blok v reťazci nazývame “genesis” blok. Genesis blok je blok, ktorý nemá žiadnych predchodcov. Aj genesis blok potrebuje PoW číslo pečate a zároveň aj predošlý hash kód, ktorý je vygenerovaný z index reťazca.

Konštruktor triedy Block nám uloží všetky potrebné dáta do bloku.

/**
 * Block implementation
 *
 * @class Block
 */
class Block {
  /**
   * Creates an instance of Block.
   *
   * @param {any} index Block index
   * @param {any} transactions Transatcions
   * @param {any} proof Proof
   * @param {any} previousHash Previous blockchain hash
   * @memberof Block
   */
  constructor (index, transactions, proof, previousHash) {
    this.index = index
    this.timestamp = Date.now()
    this.transactions = transactions
    this.proof = proof
    this.previousHash = previousHash
  }
}

module.exports = Block

Blockfactory funguje ako generátor nových blokov a je volaný z triedy Blockchain.

var Block = require('./block.js')

/**
 * BlockFactory creates block objects
 * 
 * @param {any} index Block index
 * @param {any} transactions Transatcions
 * @param {any} proof Proof
 * @param {any} previousHash Previous blockchain hash
 * @returns {Block} Block object
 */
function BlockFactory (index, transactions, proof, previousHash) {
  return new Block(index, transactions, proof, previousHash)
}

module.exports = BlockFactory

V triede blockchain metóda ‘createBlock’ zavolá Blockfactory, ktorá nasype potrebné dáta do bloku a uloží nový blok do reťazca.

/**
   * Creates block in blockchain
   * 
   * @param {any} proof         Proof
   * @param {any} previousHash  Previous blockchain hash
   * @returns {object}          Returns created block
   * @memberof Blockchain
   */
  createBlock (proof, previousHash) {
    previousHash = (typeof previousHash !== 'undefined') ? previousHash : this.createHash(chain[chain.length - 1])
    let block = BlockFactory(
      chain.length + 1,
      currentTransactions,
      proof,
      previousHash
    )

    currentTransactions = []
    chain.push(block)

    return block
  }

Pochopenie Proof of Work

Proof of Work(PoW) algoritmus hovorí o tom, ako budú nové bloky ukladané/minované. Cieľom PoW algoritmu je nájsť číslo, ktoré je riešením matematického problému. Toto číslo musí byť náročné vypočítať, resp. nájsť, ale zároveň musí byť veľmi jednoduché na overenie alebo vykonanie “skúšky správnosti”. Toto je základná idea toho, čím je PoW.

Povedzme, že hash integeru “X” vynásobený iným “Y” musí končiť nulou “0”. V príklade hash(X * Y) = ac864….0. Ako už som písal v teoretickom vysvetlení fungovania PoW, ak je X číslo predošlého bloku, počítač, resp. miner, musí ísť rad radom a overovať Y ako všetky čísla postupne, až kým nenarazí na správny výsledok. V Bitcoin blockchaine je PoW algoritmus nazývaný “Hashcash” a veľmi sa od nášho príkladu nelíši. Zároveň, ako nám už logicky vyplýva, náročnosť ťažby krytpomeny alebo konkrétne Bitcoinu sa odvíja práve od náročnosti zisťovania tohto správneho výsledku pomocou PoW algoritmu. Čím vyššie číslo, tým viac času a výpočetnej energie je vynaložené na hľadanie správneho výsledku.

Implementácia PoW

Náš PoW implementovaný v javascripte funguje na tom istom princípe ako je popísané hore. Nájsť číslo, ktoré hashovaním s predošlým číslom bloku vráti číslo so začiatočnými štyroma nulami.

/**
   * Returns last block from blockchain
   * 
   * @returns {object}      Blockchain block object
   * @memberof Blockchain
   */
  lastBlock () {
    return chain[chain.length - 1]
  }

  /**
   * Simple Proof of Work Algorithm - Hashcash
   * 
   * @param {any} lastProof Last proof
   * @returns {int}         Proof
   * @memberof Blockchain
   */
  proofOfWork (lastProof) {
    let proof = 0
    while (!this.validProof(lastProof, proof)) {
      proof += 1
    }

    return proof
  }

  /**
   * Validates the Proof
   * 
   * @param {any} lastProof Last proof
   * @param {any} proof     Proof
   * @returns {bool}        Returns true if hash starts with "0000"
   * @memberof Blockchain
   */
  validProof (lastProof, proof) {
    let guess = `${lastProof}${proof}`
    let guessHash = shajs('sha256').update(guess).digest('hex')

    return guessHash.substring(0, 4) === '0000'
  }

Ak by she chceli zvýšiť náročnosť minovania, stačí že zvýšime náročnosť PoW úpravou počtu núl. Stačí, ak by sme k štyrom nulám pridali ďalšiu a požadovali nájdenie čísla s piatimi nulami, rozdiel v náročnosti a najmä v čase potrebnom na nájdenie tohto čísla je obrovský.

Blockchain ako API

Na to, aby sme využíli potenciál toho, čo sme napísali, musíme s tým vedieť interagovať. Pre interakciu s naším blockchainom a simuláciu rôznych nodes na sieti, použijeme ExpressJS a vytvoríme si REST API.

Potrebujeme tri hlavné metódy:

/transactions/new – pre vytvorenie novej transakcie

/mine – aby sme povedali serveru aby zapečatil nový blok

/chain – aby sme mohli prezrieť dáta z celého blockchainu a overiť konsenzus

// Return current blockchain
app.get('/chain', (req, res) => {
  res.json(
    {
      'chain': blockchain.chain(),
      'links': [
        {
          'rel': 'self',
          'href': `127.0.0.1:${argv.port}/chain`
        },
        {
          'rel': 'root',
          'href': `127.0.0.1:${argv.port}`
        }
      ]
    }
  )
})

// Mine new block
app.get('/mine', (req, res, next) => {
  // Broadcast block to neighbors
  res.on('finish', () => {
    broadcast()
  })

  res.json(
    {
      'block': miner.mine(walletAddress),
      'links': [
        {
          'rel': 'self',
          'href': `127.0.0.1:${argv.port}/mine`
        },
        {
          'rel': 'root',
          'href': `127.0.0.1:${argv.port}`
        }
      ]
    }
  )
})

// Create new transaction and notify all neighbors (broadcast transaction)
app.post('/transaction/new', (req, res) => {
  let newTransaction = req.body
  if (newTransaction.sender !== undefined && newTransaction.recipient !== undefined && newTransaction.value !== undefined) {
    blockchain.addTranstacion(newTransaction.sender, newTransaction.recipient, newTransaction.value)
    broadcastTransaction(newTransaction)

    return res.json(
      {
        'transactions': blockchain.transactions(),
        'links': [
          {
            'rel': 'self',
            'href': `127.0.0.1:${argv.port}/transaction/new/`
          },
          {
            'rel': 'root',
            'href': `127.0.0.1:${argv.port}`
          }
        ]
      }
    )
  }

  return res.sendStatus(400)
})

Aby sme odoslali novú transakciu, musíme odoslať dáta v určite forme. My nepotrebujeme pre náš prípad zložitú štruktúru peňaženiek a všetkých záležitostí okolo nich, stačí nám pre demonštráciu transakcie jednoduché dáta:

{
 "sender": "my address",
 "recipient": "someone else's address",
 "amount": 5
}

Mining

Minovanie je jedna z najpodstatnejších funkcií blockchainu. V našom prípade potrebujeme spraviť tri veci:

  1. Vypočítať Proof of Work
  2. Odmeniť minera za jeho výpočet pridaním jedného coinu (+1) do jeho ‘akože’ peňaženky
  3. Vytvoriť nový blok a pridať ho do chainu
/*
* @class Miner
 */
class Miner {
  /**
   * Creates an instance of Miner.
   * 
   * @param {Blockchain} blockchain Blockchain instance
   * @memberof Miner
   */
  constructor (blockchain) {
    this.blockchain = blockchain
  }

  /**
   * Mine
   * 
   * @param {string} recipientAddress Recipient address
   * @returns {Block} Mined block 
   * @memberof Miner
   */
  mine (recipientAddress) {
    let lastBlock = this.blockchain.lastBlock()
    let lastProof = lastBlock.proof
    let proof = this.blockchain.proofOfWork(lastProof)
    this.blockchain.addTranstacion('', recipientAddress, 1)

    return this.blockchain.createBlock(proof)
  }
}

module.exports = Miner

Konsenzus algoritmus

V tomto momente máme fungujúcu nodu na sieti, s ktorou môžeme interagovať cez api, odosielať transakcie a minovať nové bloky. Celá podstata blockchainu je ale v tom, že mal by byť decentralizovaný. Keďže sieť by mala byť decentralizovaná, ako zabezpečíme, že každá node-a má ten istý chain? Tento problém sa volá problém konsenzu a jeho riešenie nám zabezpečí práve implementácia konsenzus algoritmu.

Pred tým než algoritmus implementujeme, potrebujeme vedieť o prítomnosti susedných nodes na sieti. Každý účastnik v sieti by mal držať presne tie isté dáta ako jeho “sused”. Pre tento prípad potrebujeme dopniť ďalšie api.

Najprv potrebujeme pridať novú susednú node-u.

// Add new neighbor
app.post('/neighbors/add', (req, res) => {
  let newNeighbors = req.body
  if (newNeighbors.neighbors !== undefined && newNeighbors.neighbors.constructor === Array) {
    newNeighbors.neighbors.forEach(newNeighbor => {
      neighbors.push(newNeighbor)
    })
  }

  res.json(
    {
      'neighbors': neighbors,
      'links': [
        {
          'rel': 'self',
          'href': `127.0.0.1:${argv.port}/neighbors/add`
        },
        {
          'rel': 'root',
          'href': `127.0.0.1:${argv.port}`
        }
      ]
    }
  )
})

Teraz veľmi podstatná vec, potrebujeme overiť, či naša node-a súhlasí so susednou a ak v nej nastali nejaké zmeny, čiže nastal konflikt, je potrebné ho vyriešiť. K tomu nám pomôžu ďalšie funkcie a samotný konsenzus.

/**
 * Check if provided blockchain is valid
 * 
 * @param {array} chain   Blockchain
 * @returns {bool}        Returns true if chain is valid
 * @memberof Blockchain
 */
validChain (chain) {
  let lastBlock = chain[0]
  let index = 1
  while (index < chain.length) {
    let block = chain[index]
    if (block.previousHash !== this.createHash(lastBlock)) {
      return false
    }

    if (!this.validProof(lastBlock.proof, block.proof)) {
      return false
    }

    lastBlock = block
    index += 1
  }

  return true
}

/**
 * Resolves conflicts by replacing our chain with the longest one in the network
 * 
 * @param {any} neighboringChains Array of other blockchains in the network
 * @returns {bool} Returns true if our chain is replaced with neighboring chain
 * @memberof Blockchain
 */
resolveConflicts (neighboringChains) {
  let newChain = false
  let maxLength = chain.length
  for (var index = 0; index < neighboringChains.length; ++index) {
    let neighboringChain = neighboringChains[index]
    let length = neighboringChain.length

    if (length > maxLength && this.validChain(neighboringChain)) {
      maxLength = length
      newChain = neighboringChain
    }
  }

  if (newChain) {
    chain = newChain
    // Clear current transactions to reduce possibility to write same transaction in two consecutive blocks
    currentTransactions = []

    return true
  }

  return false
}
Prvá metóda ‘validChain()’ zodpovedá za overovanie chainu a jeho validity tým, že prechádza blok za blokom, overuje hash kód a PoW číslo. Druhá metóda ‘resolveConflicts()’ je metóda, ktorá overuje susedné chainy, stiahne k sebe jeho chain a overí ho spôsobom ako pri metóde vyššie. Ak node-y nesúhlasia a chain nesie rozdielné dáta, funkcia overí, ktorý z chainov je najdhší a ten určí ako platný a ním, teda najdlhším, nahradí súčasný chain.

Interagovanie a testovanie nášho blockchaiu

Teraz máme plne funkčný blockchain a môžeme si ho nasimulovať a testovať na svojom PC. Pre účely tohto tutoriálu a pre lepší prehľad dát v chaine som pripravil UI klienta, ktorého si môžete rozbehať v prehliadači a ktorý vám zobrazí aktuálne bloky s transakciami.

Pre testovanie použijeme appku Postman alebo jednoducho terminál, aby sme mali jasný prehľad o tom, čo robíme a čo nám API vracia. Predpokladám že ste si naklonovali zdrojáky z môjho githubu, preto môžeme začať v zložke ‘example’, kde nájdete aj pod zložkou ‘app’ UI klienta do prehliadača.

V prvom rade si v terminálových oknách spustíme node-y na rôznych portoch:

#Prvé terminálové okno
$ node nodeServer.js --port 3000 --walletAddress minersWalletOne --neighbors 127.0.0.1:3001
#Blockchain Node Server started at 127.0.0.1:3000

#Druhé terminálové okno
$ node nodeServer.js --port 3001 --walletAddress minersWalletTwo --neighbors 127.0.0.1:3000
#Blockchain Node Server started at 127.0.0.1:3001

Keď niektorý z účastníkov vygeneruje nový blok, odvysiela ho do siete svojim susedom. Všetci susedia potom nahradia svoje dáta z chainu najdlhším chainom v sieti.

Keďže zatiaľ neprebehli v sieti žiadne transakcie a neboli vygenerované žiadne bloky, po spustení serverov môžeme vyminovať genesis block.

$ curl http://127.0.0.1:3000/mine
#{"block":{"index":2,"timestamp":1506944085760,"transactions":[{"sender":0,"recipient":"me","value":1}],"proof":35293,"previousHash":"50777cbc3f3d232a5be6a7de00699edb97f3a0fa399ee16a191387f3ea001af1"},"links":[{"rel":"self","href":"127.0.0.1:3000/mine"},{"rel":"root","href":"127.0.0.1:3000"}]}

Po vyminovaní môžeme overiť dáta na blockchaine a prezrieme si ich buď týmto príkazom v termináli:

$ curl http://127.0.0.1:3000/chain

alebo si môžeme teraz otvoriť UI klienta v prehliadači, ktorý nájdete v zložke ‘app’ a ten ukáže presne ako dáta v blokoch momentálne vyzerajú. Poďme ale vykonať nejaké transakcie, nech je na čo v klientovi pozerať.

Transakciu vykonáme týmto príkazom, kde si sendera, recipienta a value môžeme nahradiť vlastnými dátami, akože adresou peňaženky.

$ curl -H "Content-Type: application/json" -X POST -d '{"sender": "Ondro","recipient": "JamalJeffris","value": 500}' http://127.0.0.1:3000/transaction/new

Po vykonaní transakcií zasa dáta vyminujeme, dáta sa uložia do nových blokov a v klientovi môžeme vidieť dáta asi v takejto štruktúre:

Snímka obrazovky 2017-11-26 o 19.29.56

 

 

 

 

 

 

 

 

 

 

 

Pokiaľ vidíme všetky dáta ako sú v príklade na screenshote, všetko funguje správne. Ak by sme chceli pokračovať s vývojom ďalej, dôležité by bolo vytváranie nových peňaženiek a ich fungovanie v sieti a najmä tých, ktoré patria minerom, keďže to je miesto, kde vzniká nová hodnota našej kryptomeny.

Samozrejme, treba brať na vedomie, že tento príklad je nepoužiteľný v reálnom svete a má slúžiť len pre detailnejšie pochopenie toho, ako táto revolučná metóda funguje.

Pokiaľ ťa táto téma zaujala a chceš riešiť reálne projekty s realworld implementáciami, odporúčam prejsť moje Ethereum tutoriály:

#1 Ethereum DApp Tutorial: Intro do Smart Contractov

Related Posts