Postaw swój własny blockchain - implementacja portfela i transakcji

in #pl-artykuly7 years ago

json-blockchain.png

Implementacja portfela i transakcji


Długo się odwlekał ten post. Dni mijały a ja nie mogłem zebrać się do jego napisania. Jednak udało mi się w końcu. Do tej pory doskonale znacie poprzednie części:

A teraz nadszedł Grand Final czyli stworzenie portfela i możliwości zlecania transakcji. Przejdźmy do rzeczy, na koniec trochę pogadamy.

Jak stworzyć klucze prywatne i publiczne?

Zacznijmy od podstaw. Jest to bardzo ważne przy tworzeniu transakcji. Co to jednak tak naprawdę jest? Klucz prywatny i publiczny to nieodłączna para. Są to po prostu duże liczby. Wykorzystują magię matematyki, a dokładniej mnożenie i dzielenie. Wszyscy wiemy że o wiele łatwiej jest nam mnożyć. Weźmy prosty przykład: 123*7=? Szybko możemy policzyć, że jest to 861. W głowie same się układają liczby: 700 + 140 + 21 i mamy wynik. Teraz zobaczycie coś straszniejszego: 123/7=?. Tutaj jest już o wiele gorzej. Nie jesteśmy nawet w stanie powiedzieć dokładnie jak będzie to liczba. Widzimy że jest to coś większego od 10, w sumie 15 ale bez kartki i papieru nie obejdzie się. (cóż za staromodne podejście). Kalkulatora*

Mówiąc ogólnie o wiele łatwiej się mnoży. Tak więc wystarczy że przemnożymy jakąś wiadomość przez nasze klucze i będzie ona idealnie zaszyfrowana. My na 3 cyfrowych liczbach już nie dajemy rady dzielić w pamięci a co w wypadku gdy mamy ich kilkadziesiąt lub kilkaset.

Jako że klucz prywatny i publiczny to para. To do czego nam prywatny? Pozwala nam odszyfrować wiadomość. Wykonać właśnie to dzielenie, ponieważ mamy wszystkie składniki. Więcej o tym jest w artykule na moim blogu: https://blog.patys.pl/2017/10/27/kryptografia-blockchain-czyli-dlaczego-nikt-nie-moze-ukrasc-moich-pieniedzy-poradnik/

Przejdźmy do implementacji:

 

const secp256k1 = require('secp256k1');
const crypto = require('crypto');

Potrzebujemy dwóch bibliotek. Jedna pozwoli nam generować klucze oraz podpis do weryfikacji. Druga, czyli 'crypto' będzie hashować i zwracać nam losowe wartości.

Generowanie portfela

const generateWallet = () => {
  // generate privateKey
  let privateKey
  do {
    privateKey = crypto.randomBytes(32);
  } while (!secp256k1.privateKeyVerify(privateKey))

  // generate publicKey
  const publicKey = secp256k1.publicKeyCreate(privateKey);

  console.log('Generated keys: \nPublic key: ', publicKey.toString('hex'), '\nPrivate key: ', privateKey.toString('hex'))
  return { publicKey: publicKey.toString('hex'), privateKey: privateKey.toString('hex') };
}

 

Zróbmy sobie funkcję do tworzenia portfela. Na początek potrzebujemy klucz prywatny. Generujemy go z losowej liczby dopóki nie spełni warunków. Sprawdza nam to biblioteka. Dalej generujemy klucz publiczny na podstawie prywatnego. Pozostało tylko wypisać klucze do konsoli, żebyśmy je skopiowali. Zwracamy to jako json z kluczami zapisanymi szesnastkowo, żeby był krótszy i prostszy tekst.

Podpisywanie

const sign = (data, privateKey) => {
  const hashedData = crypto.createHash('sha256').update(JSON.stringify(data)).digest().toString('hex')
  const signedMsg = secp256k1.sign(Buffer.from(hashedData, 'hex'), Buffer.from(privateKey, 'hex'));

  if (signedMsg) {
    return signedMsg.signature.toString('hex');
  } else {
    console.error('Cannot sign');
    return null;
  }
}

No to po kolei. Do podpisania potrzebujemy jakiś danych i klucza prywatnego. Żeby mieć pewność, że dane się nie zmieniły to je hashujemy. Daje to też mniejszą ilość danych do podpisu. Następnie podpisujemy używając biblioteki. Jeśli się udało zwracamy podpis jako string zapisany szesnastkowo.

Weryfikacja danych

const verify = (data, signature, publicKey) => {
  return secp256k1.verify(Buffer.from(data, 'hex'), Buffer.from(signature, 'hex'), Buffer.from(publicKey, 'hex'));
}

Dane weryfikujemy w prosty sposób. Podajemy data, który jest hashem danych, podpis i klucz publiczny. Dzięki temu możemy potwierdzić, że na pewno osoba posiadająca klucz prywatny wrzuciła te dane.

Losowy hash

const randomHash = () => {
  return crypto.randomBytes(32).toString('hex');
}

Ta funkcja generuje losowy hash. Przyda się przy nadawaniu unikalnego id.

Transakcje


Czas coś przesłać. Musimy przygotować strukturę transakcji.

this.id = null // as a hash
this.hash = null // hash
this.sign = null
this.data = null // transaction

Zrobimy id transakcji jako losowy hash. Damy do tego hash samej transakcji, czyli hash pola data, id i sign. Do tego mamy też oczywiście podpis i same dane. W data zawrzemy informacje: kto i ile wysłał.

Przyda się funkcja do przeliczania hashu transakcji:

calculateHash() {
  const data = this.id + this.sign + this.type + JSON.stringify(this.data)
  return crypto.createHash('sha256').update(data).digest().toString('hex')
}

Poza tym sprawdzimy czy wszystkie pola są poprawne i uzupełnione:

  isValid() {
    if(!this.id || !this.hash || !this.sign || !this.data)  {
      console.error('Missing data')
      return false
    }
    if(this.hash !== this.calculateHash()) {
      console.log('Wrong hash')
      return false
    }

    const data = crypto.createHash('sha256').update(JSON.stringify(this.data)).digest().toString('hex')
    if(!cryptoHelper.verify(data, this.sign, this.data.from)) return false
    return true
  }

Sprawdzamy czy czegoś nie brakuje. Przeliczamy hash dla pewności, że jest prawidłowy oraz weryfikujemy podpis. Nie umieszczamy przecież niepodpisanej transakcji.

Ułatwienie czyli transactionBuilder


Dla ułatwienia zrobimy sobie funkcję, która pomoże nam tworzyć strukturę tych transakcji.

const createTransaction = (from, to, amount, privateKey) => {
  const order = {
    from,
    to,
    amount
  }
  const transaction = new Transaction()
  transaction.id = cryptoHelper.randomHash()
  transaction.data = order
  transaction.sign = cryptoHelper.sign(order, privateKey)
  transaction.hash = transaction.calculateHash() // hash

  if(!transaction.isValid()) {
    console.log('Transaction is invalid')
    return null
  }

  return transaction
}

Transakcja składa się z prostego zlecenia. Wskazuje tylko od kogo i do kogo idzie odpowiednia ilość pieniędzy. Następnie z tych danych jest tworzona transakcja. Dajemy jej losowe id, pole data uzupełniamy naszym zleceniem oraz podpisujemy dane i przeliczamy hash. Dla pewności sprawdzamy czy wszystko jest ok.

Czas na operacje na blockchain


Zacznijmy od początku. Nasz konstruktor blockchain wygląda teraz tak:

constructor() {
  this.blockchain = []
  this.transactions = []
  this.blockchain.push(this.generateGenesisBlock())
  this.difficulty = 4
}

Jak widzimy jest blockchain, czyli nasz główny łańcuch. Trzymamy listę transakcji, które jeszcze nie weszły do głównego łańcucha. Domyślnie chcemy aby nasz blockchain posiadał pierwszy blok oraz nadajemy trudność na 4.

Dalej jedną z nowości jest zmiana sposobu w jaki dodajemy blok. Skoro mamy już transakcję musimy je dodawać.

  addBlock() {
    const previousHash = this.getLatestBlock().hash
    const index = this.blockchain.length
    const timestamp = new Date().toISOString()

    const data = this.getTransactionsToBlock()
    if(data.length === 0) {
      console.error('No transactions in block');
      return
    }

    const newBlock = new Block(index, previousHash, timestamp, data)
    newBlock.mineBlock(this.difficulty)

    if (this.isValidBlock(newBlock, this.getLatestBlock())) {
      this.blockchain.push(newBlock)
    } else console.error('invalid block!')
  }

Struktura bloku nam się nie zmieniła. Mamy hash poprzedniego, index i timestamp. Jednak teraz uzupełniamy pole data. Wrzucamy tam nasze transakcje. Zrobiłem do tego funkcję:

  getTransactionsToBlock() {
    const transactions = []

    if(this.transactions.length > 0) {
      transactions.push(this.getFirstTransaction())
      this.removeFirstTransactionFromPending()
    } else {
      console.info('No pending transactions')
    }
    return transactions
 }

Tworzy ona tablicę i wrzuca tam ostatnią transakcję. Na razie dla uproszczenia tylko jedną. Używa dwie proste funkcje. Jedna zwraca pierwszą, czyli najstarszą transakcję a druga ją usuwa.

Dalej sprawdzamy czy coś jest w data. Jak jest to lecimy dalej i tworzymy blok. Następnie kopiemy go, czyli obliczamy odpowiedni hash. Na koniec sprawdzamy czy wszystko poszło dobrze i wrzucamy to do blockchain.

Sprawdzanie transakcji


Wyżej mamy funkcję isValidBlock. Sprawdza czy wszystko z transakcją, blokiem i blockchainem jest w porządku.

  isValidBlock(newBlock, previousBlock) {
    if (newBlock.index !== previousBlock.index + 1) {
      console.error('invalid index')
      return false
    }
    if (newBlock.previousHash !== previousBlock.hash) {
      console.error('current and previous hash dont match')
      return false
    }
    if (newBlock.hash !== newBlock.calculateHash()) {
      console.error('recalculated hash is wrong')
      return false
    }
    if (!this.areAllTransactionsValid(newBlock.data)) {
      console.error('invalid transactions')
      return false
    }
    return true
  }

Na początku sprawdzamy index, czy będzie to poprawny numer bloku. Dalej porównujemy hashe czy to dobry blok. Potem sprawdzamy czy nie nastąpiły jakieś zmiany w transakcji. Na koniec sprawdzamy czy wszystko jest w porządku z transakcją.

Co z pieniędzmi, czyli double spending
Zrobiłem funkcję, która sprawdza poprawność wszystkich transakcji w bloku.

  areAllTransactionsValid(transactions) {
    let isOk = true
    transactions.forEach(transaction => {
      if(!this.checkTransaction(TransactionBuilder.fromJSON(transaction))) {
        isOk = false
        return
      }
    })
    return isOk
 }

Bierzemy transakcje i wrzucamy je wszystkie w checkTransaction.

  checkTransaction(transaction) {
    if(!transaction) {
      console.error(`No transaction`);
      return false
    }
    console.error(`Check transaction '${transaction.id}'`);

    if(!transaction.isValid()) {
      console.error(`Transaction '${transaction.id}' is invalid`);
      return false
    }

    if(this.getMoney(transaction.data.from) < transaction.data.amount) { console.error(`Not enough money on address ${transaction.data.from}`); return false } 
    if(this.blockchain.find(block => block.data.find(data => data.id === transaction.id))) {
      console.error(`Transaction '${transaction.id}' is in blockchain`);
      return false
    }

    return true;
  }

Ta funkcja na początku sprawdza czy w ogóle mamy jakąś transakcję. Potem sprawdza czy transakcja ma odpowiednią strukturę. Dalej czy mamy odpowiednią ilość pieniędzy na swoim adresie. Na koniec przeszukujemy blockchain czy przypadkiem tam się już nie znajduje.
Zbieramy pieniądze

  getMoney(address) {
    const incomes = this.blockchain.reduce((sum, block) => {
      if(!block) return sum
      return sum + block.data.reduce((sum1, transaction) => {
        if(transaction.data.to === address) return sum1 + transaction.data.amount
        else return sum1
      }, 0)
    }, 0)

    const outcomes = this.blockchain.reduce((sum, block) => {
      if(!block) return sum
      return sum + block.data.reduce((sum1, transaction) => {
        if(transaction.data.from === address) return sum1 + transaction.data.amount
        else return sum1
      }, 0)
    }, 0)

    console.log('SUM', incomes, outcomes)

    const sum = incomes - outcomes

    if (sum < 0) console.error('Address has less then 0 money')

    return sum
  }

Ta prosta funkcja zbiera wszystkie transakcje dla danego adresu. Na początek wszystkie transakcje przychodzące, potem wychodzące, czyli wydatki. Na koniec liczy różnicę i zwraca ilość pieniędzy na adresie.

Twój blockchain


Udało się. Stworzyliśmy blockchain. Z portfelem, kopaniem, odpowiednią strukturą. Zostało nam teraz stworzyć serwer i ukazać nasz blockchain światu. Brakuje tylko podmiany blockchainów i wymiany informacji między różnymi serwerami. To będzie dalsza część projektu. Przygotuję odpowiedni kod i wystawię serwery, dzięki czemu każdy będzie mógł przetestować i sprawdzić efekt mojej pracy.

Pełen kod możecie zobaczyć tutaj: https://github.com/Patys/blockchain-transactions

Macie pomysły na zastosowanie takiego blockchaina? Pamiętajcie że zawsze można zmienić strukturę transakcji, umieścić tam dodatkowe informacje. Nie musi to być koniecznie kryptowaluta. Może to być jakiś ledger, blog, książka ze złotymi myślami. Podajcie swoje pomysły.

Sort:  

Nie wiem czy nie prościej iść w tokeny :D Generujesz w 10 minut, blockchain masz już od dawna, portfele są ... Tylko prywatyzować ;)

Myślę, że w celach uczenia zdecydowanie lepiej napisać coś własnego. Nie musi to później nigdzie działać. Najważniejsze, że wiedza zostaje.

Token każdy może mieć :p a swój własny blockchain, z własnym kodem, z własnym serwerem, napisany samemu na podstawie swojej wiedzy to już niekoniecznie. Ogólnie to robię żeby się nauczyć jak najwięcej.

Chcę zostać specjalistą od tej technologii więc muszę ją jakoś ogarnąć. Temat jest trudny i często skomplikowany. Ciężko znaleźć dobre materiały do nauki i sprowadza się to do czytania kodu gotowych blockchainów. Fajnie tak zebrać samemu tą wiedzę a potem udostępnić innym w lepszej formie.

Wygląda dobrze. Zapiszę sobie ten post i przeanalizuje dokładniej implementację wolną chwilą. Korzystałeś z jakiegoś tutoriala, czy poprostu pisałeś to z głowy?

Hej,
Ogólnie to taki zestaw miałem
https://github.com/conradoqg/naivecoin
https://github.com/lhartikk/naivechain
https://www.savjee.be/2017/09/Implementing-proof-of-work-javascript-blockchain/
https://hackernoon.com/a-cryptocurrency-implementation-in-less-than-1500-lines-of-code-d3812bedb25c

Z każdego po trochu i jakoś wyszło. Z bloga proof of work, z githuba struktura, liczenie pieniędzy, adresy. Implementacja portfela to z dokumentacji ogólnie i google.