Postaw swój własny blockchain – implementacja portfela i transakcji
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:
Moje początki z bitcoinem: https://blog.patys.pl/2017/09/16/historia-jak-poznalem-blockchain-i-bitcoin/Jak napisać własny blockchain: https://blog.patys.pl/2017/09/29/wlasny-blockchain-implementacja-poradnik/Poznać matematykę użytą w tej technologii: https://blog.patys.pl/2017/10/11/matematyka-tworzaca-blockchain-i-bitcoin-poradnik/Dowiedzieć się trochę o bezpieczeństwie: https://blog.patys.pl/2017/10/27/kryptografia-blockchain-czyli-dlaczego-nikt-nie-moze-ukrasc-moich-pieniedzy-poradnik/Zobaczyć jak to działa w praktyce: https://blog.patys.pl/2017/11/03/experty-praktyczny-przyklad-uzycie-blockchain-jako-smart-contract/Zrobić implementację Proof of Work (dowodu pracy): https://blog.patys.pl/2017/11/21/implementacja-proof-of-work-blockchain-poradnik/
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: 1237=? 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 privateKeylet privateKeydo {privateKey = crypto.randomBytes(32);} while (!secp256k1.privateKeyVerify(privateKey))// generate publicKeyconst 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 hashthis.hash = null // hashthis.sign = nullthis.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 falsereturn 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 = ordertransaction.sign = cryptoHelper.sign(order, privateKey)transaction.hash = transaction.calculateHash() // hashif(!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().hashconst index = this.blockchain.lengthconst 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 = truetransactions.forEach(transaction => {if(!this.checkTransaction(TransactionBuilder.fromJSON(transaction))) {isOk = falsereturn}})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 sumreturn sum + block.data.reduce((sum1, transaction) => {if(transaction.data.to === address) return sum1 + transaction.data.amountelse return sum1}, 0)}, 0)const outcomes = this.blockchain.reduce((sum, block) => {if(!block) return sumreturn sum + block.data.reduce((sum1, transaction) => {if(transaction.data.from === address) return sum1 + transaction.data.amountelse return sum1}, 0)}, 0)console.log('SUM', incomes, outcomes)const sum = incomes - outcomesif (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 A tutaj wersja na steemit: https://steemit.com/pl-artykuly/@patys/postaw-swoj-wlasny-blockchain-implementacja-portfela-i-transakcji
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.