Erstellen einer Kryptowährung in der Programmiersprache Crystal
Veröffentlicht: 2022-03-11Dieser Beitrag ist mein Versuch, die Schlüsselaspekte der Blockchain zu verstehen, indem ich die Interna erkunde. Ich begann mit dem Lesen des ursprünglichen Bitcoin-Whitepapers, aber ich hatte das Gefühl, dass der einzige Weg, die Blockchain wirklich zu verstehen, darin besteht, eine neue Kryptowährung von Grund auf neu zu erstellen.
Aus diesem Grund habe ich mich entschieden, eine Kryptowährung mit der neuen Programmiersprache Crystal zu erstellen, und ich habe sie CrystalCoin getauft . In diesem Artikel werden keine Algorithmusoptionen, Hash-Schwierigkeiten oder ähnliche Themen behandelt. Stattdessen wird der Fokus auf der Detaillierung eines konkreten Beispiels liegen, das ein tieferes, praktisches Verständnis der Stärken und Grenzen von Blockchains vermitteln soll.
Wenn Sie es noch nicht gelesen haben, empfehle ich Ihnen, sich für weitere Hintergrundinformationen zu Algos und Hashing den Artikel Cryptocurrency for Dummies: Bitcoin and Beyond von Demir Selmanovic anzusehen.
Warum ich mich für die Programmiersprache Crystal entschieden habe
Für eine bessere Demonstration wollte ich eine produktive Sprache wie Ruby verwenden, ohne die Leistung zu beeinträchtigen. Kryptowährung hat viele zeitaufwändige Berechnungen (nämlich Mining und Hashing ), und deshalb sind kompilierte Sprachen wie C++ und Java die Sprachen der Wahl zum Erstellen „echter“ Kryptowährungen. Abgesehen davon wollte ich eine Sprache mit einer saubereren Syntax verwenden, damit die Entwicklung Spaß macht und eine bessere Lesbarkeit ermöglicht wird. Die Kristallleistung ist sowieso gut.
Warum habe ich mich also für die Programmiersprache Crystal entschieden? Die Syntax von Crystal ist stark von der von Ruby inspiriert, daher fühlt es sich für mich natürlich an zu lesen und einfach zu schreiben. Es hat den zusätzlichen Vorteil einer geringeren Lernkurve, insbesondere für erfahrene Ruby-Entwickler.
So drückt es das Crystal Lang Team auf seiner offiziellen Website aus:
Schnell wie C, glatt wie Ruby.
Im Gegensatz zu Ruby oder JavaScript, die interpretierte Sprachen sind, ist Crystal jedoch eine kompilierte Sprache, was sie viel schneller macht und einen geringeren Speicherbedarf bietet. Unter der Haube verwendet es LLVM zum Kompilieren in nativen Code.
Crystal ist auch statisch typisiert, was bedeutet, dass der Compiler Ihnen hilft, Typfehler während der Kompilierung abzufangen.
Ich werde nicht erklären, warum ich die Crystal-Sprache für großartig halte, da dies den Rahmen dieses Artikels sprengen würde, aber wenn Sie meinen Optimismus nicht überzeugend finden, können Sie sich diesen Artikel ansehen, um einen besseren Überblick über das Potenzial von Crystal zu erhalten.
Hinweis: In diesem Artikel wird davon ausgegangen, dass Sie bereits über grundlegende Kenntnisse der objektorientierten Programmierung (OOP) verfügen.
Blockchain
Also, was ist eine Blockchain? Es ist eine Liste (Kette) von Blöcken, die durch digitale Fingerabdrücke (auch bekannt als Krypto-Hashes) verknüpft und gesichert sind.
Am einfachsten kann man es sich als Datenstruktur einer verketteten Liste vorstellen. Davon abgesehen muss eine verknüpfte Liste nur einen Verweis auf das vorherige Element haben; Ein Block muss einen Identifikator haben, der vom Identifikator des vorherigen Blocks abhängt, was bedeutet, dass Sie einen Block nicht ersetzen können, ohne jeden einzelnen nachfolgenden Block neu zu berechnen.
Stellen Sie sich Blockchain vorerst als eine Reihe von Blöcken vor, bei denen einige Daten mit einer Kette verknüpft sind, wobei die Kette der Hash des vorherigen Blocks ist.
Die gesamte Blockchain würde auf jedem Knoten existieren, der mit ihr interagieren möchte, was bedeutet, dass sie auf jeden der Knoten im Netzwerk kopiert wird. Kein einzelner Server hostet es, aber alle Blockchain-Entwicklungsunternehmen verwenden es, wodurch es dezentralisiert wird.
Ja, das ist seltsam im Vergleich zu herkömmlichen zentralisierten Systemen. Jeder der Knoten wird eine Kopie der gesamten Blockchain haben (> 149 GB in der Bitcoin-Blockchain bis Dezember 2017).
Hashing und digitale Signatur
Also, was ist diese Hash-Funktion? Stellen Sie sich den Hash als eine Funktion vor, die einen eindeutigen Fingerabdruck zurückgibt, wenn wir ihr einen Text/ein Objekt geben. Selbst die kleinste Änderung des Eingabeobjekts würde den Fingerabdruck dramatisch verändern.
Es gibt verschiedene Hash-Algorithmen, und in diesem Artikel verwenden wir den SHA256
Hash-Algorithmus, der in Bitcoin
verwendet wird.
Mit SHA256
ergeben wir immer eine Länge von 64 Hexadezimalzeichen (256 Bit), auch wenn die Eingabe weniger als 256 Bit oder viel größer als 256 Bit ist:
Eingang | Gehashte Ergebnisse |
---|---|
SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT SEHR LANGER TEXT | cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a |
Toptal | 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21 |
Toptal. | 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0 |
Beachten Sie beim letzten Beispiel, dass nur das Hinzufügen einer .
(Punkt) führte zu einer dramatischen Veränderung des Hashs.
Daher wird in einer Blockchain die Kette aufgebaut, indem die Blockdaten an einen Hash-Algorithmus übergeben werden, der einen Hash generiert, der mit dem nächsten Block verknüpft wird und fortan eine Reihe von Blöcken bildet, die mit den Hashes der vorherigen Blöcke verknüpft sind.
Aufbau einer Kryptowährung in Crystal
Beginnen wir nun mit der Erstellung unseres Crystal-Projekts und der Erstellung unserer SHA256
-Verschlüsselung.
Angenommen, Sie haben die Programmiersprache Crystal installiert, lassen Sie uns das Skelett der CrystalCoin
-Codebasis erstellen, indem wir Crystals integriertes Projektwerkzeug crystal init app [name]
:
% crystal init app crystal_coin create crystal_coin/.gitignore create crystal_coin/.editorconfig create crystal_coin/LICENSE create crystal_coin/README.md create crystal_coin/.travis.yml create crystal_coin/shard.yml create crystal_coin/src/crystal_coin.cr create crystal_coin/src/crystal_coin/version.cr create crystal_coin/spec/spec_helper.cr create crystal_coin/spec/crystal_coin_spec.cr Initialized empty Git repository in /Users/eki/code/crystal_coin/.git/
Dieser Befehl erstellt die Grundstruktur für das Projekt mit einem bereits initialisierten Git-Repository, Lizenz- und Readme-Dateien. Es enthält auch Stubs für Tests und die Datei shard.yml
zur Beschreibung des Projekts und zur Verwaltung von Abhängigkeiten, die auch als Shards bezeichnet werden.
Fügen wir den openssl
-Shard hinzu, der zum Erstellen des SHA256
-Algorithmus benötigt wird:
# shard.yml dependencies: openssl: github: datanoise/openssl.cr
Sobald das drin ist, gehen Sie zurück in Ihr Terminal und führen crystal deps
. Dadurch werden openssl
und seine Abhängigkeiten heruntergezogen, damit wir sie verwenden können.
Jetzt haben wir die erforderliche Bibliothek in unserem Code installiert. Beginnen wir mit der Definition der Block
-Klasse und dem Erstellen der Hash-Funktion.
# src/crystal_coin/block.cr require "openssl" module CrystalCoin class Block def initialize(data : String) @data = data end def hash hash = OpenSSL::Digest.new("SHA256") hash.update(@data) hash.hexdigest end end end puts CrystalCoin::Block.new("Hello, Cryptos!").hash
Sie können Ihre Anwendung jetzt testen, indem Sie Crystal run crystal src/crystal_coin/block.cr
von Ihrem Terminal aus ausführen.
crystal_coin [master●] % crystal src/crystal_coin/block.cr 33eedea60b0662c66c289ceba71863a864cf84b00e10002ca1069bf58f9362d5
Gestaltung unserer Blockchain
Jeder Block wird mit einem timestamp
und optional einem index
gespeichert. In CrystalCoin
werden wir beide speichern. Um die Integrität in der gesamten Blockchain zu gewährleisten, hat jeder Block einen selbstidentifizierenden Hash . Wie bei Bitcoin ist der Hash jedes Blocks ein kryptografischer Hash des Blocks ( index
, timestamp
, data
und der Hash des Hash des vorherigen Blocks previous_hash
). Die Daten können vorerst alles sein, was Sie wollen.
module CrystalCoin class Block property current_hash : String def initialize(index = 0, data = "data", previous_hash = "hash") @data = data @index = index @timestamp = Time.now @previous_hash = previous_hash @current_hash = hash_block end private def hash_block hash = OpenSSL::Digest.new("SHA256") hash.update("#{@index}#{@timestamp}#{@data}#{@previous_hash}") hash.hexdigest end end end puts CrystalCoin::Block.new(data: "Same Data").current_hash
In Crystal lang ersetzen wir Rubys Methoden attr_accessor
, attr_getter
und attr_setter
durch neue Schlüsselwörter:
Ruby-Schlüsselwort | Kristall-Schlüsselwort |
---|---|
attr_accessor | Eigentum |
attr_reader | Getter |
attr_writer | Setter |
Eine andere Sache, die Sie vielleicht in Crystal bemerkt haben, ist, dass wir den Compiler durch unseren Code auf bestimmte Typen hinweisen möchten. Crystal leitet die Typen ab, aber wenn Sie Mehrdeutigkeiten haben, können Sie Typen auch explizit deklarieren. Aus diesem Grund haben wir String
-Typen für current_hash
hinzugefügt.
Lassen Sie uns jetzt block.cr
zweimal ausführen und beachten Sie, dass dieselben Daten aufgrund des unterschiedlichen timestamp
unterschiedliche Hashes erzeugen:
crystal_coin [master●] % crystal src/crystal_coin/block.cr 361d0df74e28d37b71f6c5f579ee182dd3d41f73f174dc88c9f2536172d3bb66 crystal_coin [master●] % crystal src/crystal_coin/block.cr b1fafd81ba13fc21598fb083d9429d1b8a7e9a7120dbdacc7e461791b96b9bf3
Jetzt haben wir unsere Blockstruktur, aber wir erstellen eine Blockchain. Wir müssen anfangen, Blöcke hinzuzufügen, um eine tatsächliche Kette zu bilden. Wie ich bereits erwähnt habe, benötigt jeder Block Informationen aus dem vorherigen Block. Aber wie kommt der erste Block in der Blockchain dorthin? Nun, der erste Block oder genesis
-Block ist ein spezieller Block (ein Block ohne Vorgänger). In vielen Fällen wird es manuell hinzugefügt oder verfügt über eine eindeutige Logik, die das Hinzufügen ermöglicht.
Wir erstellen eine neue Funktion, die einen Genesis-Block zurückgibt. Dieser Block hat index=0
und einen beliebigen Datenwert und einen beliebigen Wert im Parameter previous_hash
.
Lassen Sie uns die Methode Block.first
erstellen oder klassen, die den Genesis-Block generiert:
module CrystalCoin class Block ... def self.first(data="Genesis Block") Block.new(data: data, previous_hash: "0") end ... end end
Und testen wir es mit p CrystalCoin::Block.first
:
#<CrystalCoin::Block:0x10b33ac80 @current_hash="acb701a9b70cff5a0617d654e6b8a7155a8c712910d34df692db92455964d54e", @data="Genesis Block", @index=0, @timestamp=2018-05-13 17:54:02 +03:00, @previous_hash="0">
Jetzt, da wir in der Lage sind, einen Genesis -Block zu erstellen, brauchen wir eine Funktion, die nachfolgende Blöcke in der Blockchain generiert.
Diese Funktion nimmt den vorherigen Block in der Kette als Parameter, erstellt die Daten für den zu generierenden Block und gibt den neuen Block mit den entsprechenden Daten zurück. Wenn neue Blöcke Informationen aus vorherigen Blöcken hashen, erhöht sich die Integrität der Blockchain mit jedem neuen Block.
Eine wichtige Konsequenz ist, dass ein Block nicht geändert werden kann, ohne den Hash jedes aufeinanderfolgenden Blocks zu ändern. Dies wird im folgenden Beispiel demonstriert. Wenn die Daten in Block 44 von LOOP
auf EAST
geändert werden, müssen alle Hashes der aufeinanderfolgenden Blöcke geändert werden. Dies liegt daran, dass der Hash des Blocks (unter anderem) vom Wert des previous_hash
abhängt.
Wenn wir dies nicht tun würden, wäre es für eine externe Partei einfacher, die Daten zu ändern und unsere Kette durch eine völlig neue ihrer eigenen zu ersetzen. Diese Hash-Kette dient als kryptografischer Beweis und hilft sicherzustellen, dass ein einmal zur Blockchain hinzugefügter Block nicht ersetzt oder entfernt werden kann. Lassen Sie uns die Klassenmethode Block.next
:
module CrystalCoin class Block ... def self.next(previous_node, data = "Transaction Data") Block.new( data: "Transaction data number (#{previous_node.index + 1})", index: previous_node.index + 1, previous_hash: previous_hash.hash ) end ... end end
Um das alles gemeinsam auszuprobieren, erstellen wir eine einfache Blockchain. Das erste Element der Liste ist der Genesis-Block. Und natürlich müssen wir die nachfolgenden Blöcke hinzufügen. Wir werden fünf neue Blöcke erstellen, um CrystalCoin
zu demonstrieren:
blockchain = [ CrystalCoin::Block.first ] previous_block = blockchain[0] 5.times do new_block = CrystalCoin::Block.next(previous_block: previous_block) blockchain << new_block previous_block = new_block end p blockchain
[#<CrystalCoin::Block:0x108c57c80 @current_hash= "df7f9d47bee95c9158e3043ddd17204e97ccd6e8958e4e41dacc7f0c6c0df610", @index=0, @previous_hash="0", @timestamp=2018-06-04 12:13:21 +03:00, @data="Genesis Block>, #<CrystalCoin::Block:0x109c89740 @current_hash= "d188fcddd056c044c783d558fd6904ceeb9b2366339af491a293d369de4a81f6", @index=1, @previous_hash= "df7f9d47bee95c9158e3043ddd17204e97ccd6e8958e4e41dacc7f0c6c0df610", @timestamp=2018-06-04 12:13:21 +03:00, @data="Transaction data number (1)">, #<CrystalCoin::Block:0x109cc8500 @current_hash= "0b71b61118b9300b4fe8cdf4a7cbcc1dac4da7a8a3150aa97630994805354107", @index=2, @previous_hash= "d188fcddd056c044c783d558fd6904ceeb9b2366339af491a293d369de4a81f6", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (2)">, #<CrystalCoin::Block:0x109ccbe40 @current_hash= "9111406deea4f07f807891405078a3f8537416b31ab03d78bda3f86d9ae4c584", @index=3, @previous_hash= "0b71b61118b9300b4fe8cdf4a7cbcc1dac4da7a8a3150aa97630994805354107", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (3)">, #<CrystalCoin::Block:0x109cd0980 @current_hash= "0063bfc5695c0d49b291a8813c566b047323f1808a428e0eb1fca5c399547875", @index=4, @previous_hash= "9111406deea4f07f807891405078a3f8537416b31ab03d78bda3f86d9ae4c584", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (4)">, #<CrystalCoin::Block:0x109cd0100 @current_hash= "00a0c70e5412edd7389a0360b48c407ce4ddc8f14a0bcf16df277daf3c1a00c7", @index=5, @previous_hash= "0063bfc5695c0d49b291a8813c566b047323f1808a428e0eb1fca5c399547875", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (5)">
Arbeitsnachweis
Mit einem Proof-of-Work-Algorithmus (PoW) werden neue Blöcke in der Blockchain erstellt oder abgebaut . Das Ziel von PoW ist es, eine Zahl zu finden, die ein Problem löst. Die Nummer muss schwer zu finden, aber von jedem im Netzwerk rechnerisch leicht zu überprüfen sein. Das ist die Kernidee hinter Proof of Work.
Lassen Sie uns anhand eines Beispiels demonstrieren, um sicherzustellen, dass alles klar ist. Wir gehen davon aus, dass der Hash einer Ganzzahl x multipliziert mit einer anderen y mit 00
beginnen muss. Damit:
hash(x * y) = 00ac23dc...
Und für dieses vereinfachte Beispiel fixieren wir x=5
und implementieren dies in Crystal:
x = 5 y = 0 while hash((x*y).to_s)[0..1] != "00" y += 1 end puts "The solution is y = #{y}" puts "Hash(#{x}*#{y}) = #{hash((x*y).to_s)}"
Lassen Sie uns den Code ausführen:
crystal_coin [master●●] % time crystal src/crystal_coin/pow.cr The solution is y = 530 Hash(5*530) = 00150bc11aeeaa3cdbdc1e27085b0f6c584c27e05f255e303898dcd12426f110 crystal src/crystal_coin/pow.cr 1.53s user 0.23s system 160% cpu 1.092 total
Wie Sie sehen können, war diese Zahl y=530
schwer zu finden (Brute-Force), aber einfach mit der Hash-Funktion zu überprüfen.
Warum sollte man sich mit diesem PoW-Algorithmus beschäftigen? Warum erstellen wir nicht einfach einen Hash pro Block und das war's? Ein Hash muss gültig sein. In unserem Fall ist ein Hash gültig, wenn die ersten beiden Zeichen unseres Hashs 00
sind. Wenn unser Hash mit 00......
beginnt, gilt er als gültig. Dies wird als Schwierigkeit bezeichnet. Je höher die Schwierigkeit, desto länger dauert es, einen gültigen Hash zu erhalten.
Aber wenn der Hash beim ersten Mal nicht gültig ist, muss sich etwas an den von uns verwendeten Daten ändern. Wenn wir immer wieder die gleichen Daten verwenden, erhalten wir immer wieder den gleichen Hash und unser Hash wird niemals gültig sein. Wir verwenden etwas namens nonce
in unserem Hash (in unserem vorherigen Beispiel ist es das y
). Es ist einfach eine Zahl, die wir jedes Mal erhöhen, wenn der Hash nicht gültig ist. Wir erhalten unsere Daten (Datum, Nachricht, vorheriger Hash, Index) und eine Nonce von 1. Wenn der Hash, den wir damit erhalten, nicht gültig ist, versuchen wir es mit einer Nonce von 2. Und wir erhöhen die Nonce, bis wir einen gültigen Hash erhalten .
Bei Bitcoin heißt der Proof-of-Work-Algorithmus Hashcash. Lassen Sie uns unserer Block-Klasse einen Proof-of-Work hinzufügen und mit dem Mining beginnen, um die Nonce zu finden. Wir verwenden einen fest codierten Schwierigkeitsgrad mit zwei führenden Nullen '00':
Lassen Sie uns nun unsere Block-Klasse neu gestalten, um dies zu unterstützen. Unser CrystalCoin
Block enthält die folgenden Attribute:
1) index: indicates the index of the block ex: 0,1 2) timestamp: timestamp in epoch, number of seconds since 1 Jan 1970 3) data: the actual data that needs to be stored on the blockchain. 4) previous_hash: the hash of the previous block, this is the chain/link between the blocks 5) nonce: this is the number that is to be mined/found. 6) current_hash: The hash value of the current block, this is generated by combining all the above attributes and passing it to a hashing algorithm
Ich werde ein separates Modul erstellen, um das Hashing durchzuführen und die nonce
zu finden, damit wir unseren Code sauber und modular halten. Ich nenne es proof_of_work.cr
:
require "openssl" module CrystalCoin module ProofOfWork private def proof_of_work(difficulty = "00") nonce = 0 loop do hash = calc_hash_with_nonce(nonce) if hash[0..1] == difficulty return nonce else nonce += 1 end end end private def calc_hash_with_nonce(nonce = 0) sha = OpenSSL::Digest.new("SHA256") sha.update("#{nonce}#{@index}#{@timestamp}#{@data}#{@previous_hash}") sha.hexdigest end end end
Unsere Block
-Klasse würde in etwa so aussehen:
require "./proof_of_work" module CrystalCoin class Block include ProofOfWork property current_hash : String property index : Int32 property nonce : Int32 property previous_hash : String def initialize(index = 0, data = "data", previous_hash = "hash") @data = data @index = index @timestamp = Time.now @previous_hash = previous_hash @nonce = proof_of_work @current_hash = calc_hash_with_nonce(@nonce) end def self.first(data = "Genesis Block") Block.new(data: data, previous_hash: "0") end def self.next(previous_block, data = "Transaction Data") Block.new( data: "Transaction data number (#{previous_block.index + 1})", index: previous_block.index + 1, previous_hash: previous_block.current_hash ) end end end
Einige Dinge, die zu Crystal-Code und Crystal-Sprachbeispielen im Allgemeinen zu beachten sind. In Crystal sind Methoden standardmäßig öffentlich. Crystal erfordert, dass jeder privaten Methode das Schlüsselwort private vorangestellt wird, was bei Ruby verwirrend sein könnte.
Sie haben vielleicht bemerkt, dass die Integer-Typen von Crystal Int8
, Int16
, Int32
, Int64
, UInt8
, UInt16
, UInt32
oder UInt64
im Vergleich zu Rubys Fixnum
. true
und false
sind Werte in der Bool
-Klasse und nicht Werte in den Klassen TrueClass
oder FalseClass
in Ruby.
Crystal hat optionale und benannte Methodenargumente als zentrale Sprachfunktionen und erfordert keinen speziellen Code für die Behandlung der Argumente, was ziemlich cool ist. Schauen Sie sich Block#initialize(index = 0, data = "data", previous_hash = "hash")
an und rufen Sie es dann mit etwas wie Block.new(data: data, previous_hash: "0")
.
Eine detailliertere Liste der Unterschiede zwischen der Programmiersprache Crystal und Ruby finden Sie unter Crystal für Rubyisten.
Versuchen wir nun, fünf Transaktionen zu erstellen, indem wir Folgendes verwenden:
blockchain = [ CrystalCoin::Block.first ] puts blockchain.inspect previous_block = blockchain[0] 5.times do |i| new_block = CrystalCoin::Block.next(previous_block: previous_block) blockchain << new_block previous_block = new_block puts new_block.inspect end
[#<CrystalCoin::Block:0x108f8fea0 @current_hash="0088ca080a49334e1cb037ed4c42795d635515ef1742e6bcf439bf0f95711759", @index=0, @nonce=17, @timestamp=2018-05-14 17:20:46 +03:00, @data="Genesis Block", @previous_hash="0">] #<CrystalCoin::Block:0x108f8f660 @current_hash="001bc2b04d7ad8ef25ada30e2bde19d7bbbbb3ad42348017036b0d4974d0ccb0", @index=1, @nonce=24, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (1)", @previous_hash="0088ca080a49334e1cb037ed4c42795d635515ef1742e6bcf439bf0f95711759"> #<CrystalCoin::Block:0x109fc5ba0 @current_hash="0019256c998028111838b872a437cd8adced53f5e0f8f43388a1dc4654844fe5", @index=2, @nonce=61, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (2)", @previous_hash="001bc2b04d7ad8ef25ada30e2bde19d7bbbbb3ad42348017036b0d4974d0ccb0"> #<CrystalCoin::Block:0x109fdc300 @current_hash="0080a30d0da33426a1d4f36d870d9eb709eaefb0fca62cc68e497169c5368b97", @index=3, @nonce=149, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (3)", @previous_hash="0019256c998028111838b872a437cd8adced53f5e0f8f43388a1dc4654844fe5"> #<CrystalCoin::Block:0x109ff58a0 @current_hash="00074399d51c700940e556673580a366a37dec16671430141f6013f04283a484", @index=4, @nonce=570, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (4)", @previous_hash="0080a30d0da33426a1d4f36d870d9eb709eaefb0fca62cc68e497169c5368b97"> #<CrystalCoin::Block:0x109fde120 @current_hash="00720bb6e562a25c19ecd2b277925057626edab8981ff08eb13773f9bb1cb842", @index=5, @nonce=475, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (5)", @previous_hash="00074399d51c700940e556673580a366a37dec16671430141f6013f04283a484">
Sieh den Unterschied? Jetzt beginnen alle Hashes mit 00
. Das ist die Magie des Proof-of-Work. Unter Verwendung ProofOfWork
wir die nonce
gefunden und der Beweis ist der Hash mit der übereinstimmenden Schwierigkeit, d. h. den beiden führenden Nullen 00
.
Beachten Sie, dass wir beim ersten Block, den wir erstellt haben, 17 Nonces ausprobiert haben, bis wir die passende Glückszahl gefunden haben:
Block | Schleifen / Anzahl der Hash-Berechnungen |
---|---|
#0 | 17 |
#1 | 24 |
#2 | 61 |
#3 | 149 |
#4 | 570 |
#5 | 475 |
Versuchen wir es jetzt mit einer Schwierigkeit von vier führenden Nullen ( difficulty="0000"
):

Block | Schleifen / Anzahl der Hash-Berechnungen |
---|---|
#1 | 26 762 |
#2 | 68 419 |
#3 | 23 416 |
#4 | 15 353 |
Im ersten Block versucht man 26762 Nonces (vergleiche 17 Nonces mit Schwierigkeit '00') bis man die passende Glückszahl findet.
Unsere Blockchain als API
So weit, ist es gut. Wir haben unsere einfache Blockchain erstellt und es war relativ einfach. Das Problem hierbei ist jedoch, dass CrystalCoin
nur auf einem einzigen Computer ausgeführt werden kann (es ist nicht verteilt/dezentralisiert).
Ab sofort verwenden wir JSON-Daten für CrystalCoin
. Die Daten sind Transaktionen, also ist das Datenfeld jedes Blocks eine Liste von Transaktionen.
Jede Transaktion ist ein JSON-Objekt, das den sender
der Münze, den receiver
der Münze und die amount
an CrystalCoin, die übertragen wird, angibt:
{ "from": "71238uqirbfh894-random-public-key-a-alkjdflakjfewn204ij", "to": "93j4ivnqiopvh43-random-public-key-b-qjrgvnoeirbnferinfo", "amount": 3 }
Einige Änderungen an unserer Block
-Klasse zur Unterstützung des neuen transaction
(früher data
genannt). Um Verwirrung zu vermeiden und Konsistenz zu wahren, verwenden wir von nun an den Begriff transaction
, um auf data
zu verweisen, die in unserer Beispielanwendung veröffentlicht werden.
Wir führen eine neue einfache Klasse Transaction
ein:
module CrystalCoin class Block class Transaction property from : String property to : String property amount : Int32 def initialize(@from, @to, @amount) end end end end
Die Transaktionen werden in Blöcke gepackt, sodass ein Block nur eine oder mehrere Transaktionen enthalten kann. Blöcke, die die Transaktionen enthalten, werden häufig generiert und der Blockchain hinzugefügt.
Die Blockchain soll eine Sammlung von Blöcken sein. Wir können alle Blöcke in der Crystal-Liste speichern, und deshalb führen wir die neue Klasse Blockchain
ein:
Blockchain
wird chain
und uncommitted_transactions
-Arrays haben. Die chain
enthält alle abgebauten Blöcke in der Blockchain, und uncommitted_transactions
enthält alle Transaktionen, die nicht zur Blockchain hinzugefügt wurden (noch nicht abgebaut wurden). Sobald wir Blockchain
initialisiert haben, erstellen wir den Genesis-Block mit Block.first
und fügen ihn dem chain
hinzu, und wir fügen ein leeres Array uncommitted_transactions
hinzu.
Wir werden die Blockchain#add_transaction
Methode erstellen, um Transaktionen zum uncommitted_transactions
-Array hinzuzufügen.
Lassen Sie uns unsere neue Blockchain
-Klasse erstellen:
require "./block" require "./transaction" module CrystalCoin class Blockchain getter chain getter uncommitted_transactions def initialize @chain = [ Block.first ] @uncommitted_transactions = [] of Block::Transaction end def add_transaction(transaction) @uncommitted_transactions << transaction end end end
In der Block
-Klasse beginnen wir mit der Verwendung von transactions
anstelle von data
:
module CrystalCoin class Block include ProofOfWork def initialize(index = 0, transactions = [] of Transaction, previous_hash = "hash") @transactions = transactions ... end .... def self.next(previous_block, transactions = [] of Transaction) Block.new( transactions: transactions, index: previous_block.index + 1, previous_hash: previous_block.current_hash ) end end end
Jetzt, da wir wissen, wie unsere Transaktionen aussehen werden, brauchen wir eine Möglichkeit, sie zu einem der Computer in unserem Blockchain-Netzwerk hinzuzufügen, der als node
bezeichnet wird. Dazu erstellen wir einen einfachen HTTP-Server.
Wir erstellen vier Endpunkte:
- [POST]
/transactions/new
: um eine neue Transaktion für einen Block zu erstellen - [GET]
/mine
: um unserem Server mitzuteilen, dass er einen neuen Block abbauen soll. - [GET]
/chain
: um die vollständige Blockchain imJSON
-Format zurückzugeben. - [GET]
/pending
: um die ausstehenden Transaktionen zurückzugeben (uncommitted_transactions
).
Wir werden das Kemal-Webframework verwenden. Es handelt sich um ein Mikro-Framework, das die Zuordnung von Endpunkten zu Crystal-Funktionen vereinfacht. Kemal ist stark von Sinatra for Rubyists beeinflusst und arbeitet sehr ähnlich. Wenn Sie nach einem Äquivalent zu Ruby on Rails suchen, dann schauen Sie sich Amber an.
Unser Server bildet einen einzelnen Knoten in unserem Blockchain-Netzwerk. Lassen Sie uns zuerst Kemal
zur Datei shard.yml
als hinzufügen und die Abhängigkeit installieren:
dependencies: kemal: github: kemalcr/kemal
Lassen Sie uns nun das Skelett unseres HTTP-Servers erstellen:
# src/server.cr require "kemal" require "./crystal_coin" # Generate a globally unique address for this node node_identifier = UUID.random.to_s # Create our Blockchain blockchain = Blockchain.new get "/chain" do "Send the blockchain as json objects" end get "/mine" do "We'll mine a new Block" end get "/pending" do "Send pending transactions as json objects" end post "/transactions/new" do "We'll add a new transaction" end Kemal.run
Und führen Sie den Server aus:
crystal_coin [master●●] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000
Stellen wir sicher, dass der Server einwandfrei funktioniert:
% curl http://0.0.0.0:3000/chain Send the blockchain as json objects%
So weit, ist es gut. Jetzt können wir mit der Implementierung der einzelnen Endpunkte fortfahren. Beginnen wir mit der Implementierung von /transactions/new
und pending
Endpunkten:
get "/pending" do { transactions: blockchain.uncommitted_transactions }.to_json end post "/transactions/new" do |env| transaction = CrystalCoin::Block::Transaction.new( from: env.params.json["from"].as(String), to: env.params.json["to"].as(String), amount: env.params.json["amount"].as(Int64) ) blockchain.add_transaction(transaction) "Transaction #{transaction} has been added to the node" end
Unkomplizierte Umsetzung. Wir erstellen einfach ein CrystalCoin::Block::Transaction
-Objekt und fügen die Transaktion mit Blockchain#add_transaction
uncommitted_transactions
Array uncommitted_transactions hinzu.
Im Moment werden die Transaktionen zunächst in einem Pool von uncommitted_transactions
gespeichert. Der Prozess, unbestätigte Transaktionen in einen Block zu packen und Proof of Work (PoW) zu berechnen, wird als Mining von Blöcken bezeichnet. Sobald die nonce
, die unsere Einschränkungen erfüllt, herausgefunden ist, können wir sagen, dass ein Block abgebaut wurde, und der neue Block wird in die Blockchain eingefügt.
In CrystalCoin
verwenden wir den einfachen Proof-of-Work-Algorithmus, den wir zuvor erstellt haben. Um einen neuen Block zu erstellen, muss der Computer eines Miners:
- Finden Sie den letzten Block in der
chain
. - Finden Sie ausstehende Transaktionen (
uncommitted_transactions
). - Erstellen Sie mit
Block.next
einen neuen Block. - Fügen Sie den abgebauten Block zum
chain
hinzu. - Bereinigen Sie das
uncommitted_transactions
-Array.
Um den /mine
-Endpunkt zu implementieren, implementieren wir zunächst die obigen Schritte in Blockchain#mine
:
module CrystalCoin class Blockchain include Consensus BLOCK_SIZE = 25 ... def mine raise "No transactions to be mined" if @uncommitted_transactions.empty? new_block = Block.next( previous_block: @chain.last, transactions: @uncommitted_transactions.shift(BLOCK_SIZE) ) @chain << new_block end end end
Wir stellen zunächst sicher, dass wir einige ausstehende Transaktionen zum Minen haben. Dann erhalten wir den letzten Block mit @chain.last
und die ersten 25
Transaktionen, die abgebaut werden sollen (wir verwenden Array#shift(BLOCK_SIZE)
, um ein Array der ersten 25 uncommitted_transactions
zurückzugeben, und entfernen dann die Elemente, die bei Index 0 beginnen). .
Lassen Sie uns nun den /mine
-Endpunkt implementieren:
get "/mine" do blockchain.mine "Block with index=#{blockchain.chain.last.index} is mined." end
Und für /chain
-Endpunkt:
get "/chain" do { chain: blockchain.chain }.to_json end
Interaktion mit unserer Blockchain
Wir verwenden cURL
, um mit unserer API über ein Netzwerk zu interagieren.
Lassen Sie uns zuerst den Server hochfahren:
crystal_coin [master] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000
Dann erstellen wir zwei neue Transaktionen, indem wir eine POST
-Anfrage an http://localhost:3000/transactions/new
mit einem Body senden, der unsere Transaktionsstruktur enthält:
crystal_coin [master●] % curl -X POST http://0.0.0.0:3000/transactions/new -H "Content-Type: application/json" -d '{"from": "eki", "to":"iron_man", "amount": 1000}' Transaction #<CrystalCoin::Block::Transaction:0x10c4159f0 @from="eki", @to="iron_man", @amount=1000_i64> has been added to the node% crystal_coin [master●] % curl -X POST http://0.0.0.0:3000/transactions/new -H "Content-Type: application/json" -d '{"from": "eki", "to":"hulk", "amount": 700}' Transaction #<CrystalCoin::Block::Transaction:0x10c415810 @from="eki", @to="hulk", @amount=700_i64> has been added to the node%
Lassen Sie uns nun die ausstehenden Transaktionen auflisten (dh Transaktionen, die dem Block noch nicht hinzugefügt wurden):
crystal_coin [master●] % curl http://0.0.0.0:3000/pendings { "transactions":[ { "from":"ekis", "to":"huslks", "amount":7090 }, { "from":"ekis", "to":"huslks", "amount":70900 } ] }
Wie wir sehen können, wurden die beiden zuvor erstellten Transaktionen zu uncommitted_transactions
hinzugefügt.
Lassen Sie uns nun die beiden Transaktionen minen , indem Sie eine GET
-Anfrage an http://0.0.0.0:3000/mine
:
crystal_coin [master●] % curl http://0.0.0.0:3000/mine Block with index=1 is mined.
Sieht so aus, als hätten wir den ersten Block erfolgreich abgebaut und zu unserer chain
hinzugefügt. Lassen Sie uns unsere chain
noch einmal überprüfen und ob sie den abgebauten Block enthält:
crystal_coin [master●] % curl http://0.0.0.0:3000/chain { "chain": [ { "index": 0, "current_hash": "00d469d383005b4303cfa7321c02478ce76182564af5d16e1a10d87e31e2cb30", "nonce": 363, "previous_hash": "0", "transactions": [ ], "timestamp": "2018-05-23T01:59:52+0300" }, { "index": 1, "current_hash": "003c05da32d3672670ba1e25ecb067b5bc407e1d5f8733b5e33d1039de1a9bf1", "nonce": 320, "previous_hash": "00d469d383005b4303cfa7321c02478ce76182564af5d16e1a10d87e31e2cb30", "transactions": [ { "from": "ekis", "to": "huslks", "amount": 7090 }, { "from": "ekis", "to": "huslks", "amount": 70900 } ], "timestamp": "2018-05-23T02:02:38+0300" } ] }
Konsens und Dezentralisierung
Das ist cool. Wir haben uns eine grundlegende Blockchain zugelegt, die Transaktionen akzeptiert und es uns ermöglicht, neue Blöcke abzubauen. Aber der Code, den wir bisher implementiert haben, soll auf einem einzelnen Computer laufen, während der springende Punkt bei Blockchains darin besteht, dass sie dezentralisiert sein sollten. Aber wenn sie dezentralisiert sind, wie stellen wir dann sicher, dass sie alle dieselbe Kette widerspiegeln?
Das ist das Problem des Consensus
.
Wir müssen einen Konsensalgorithmus implementieren, wenn wir mehr als einen Knoten in unserem Netzwerk haben wollen.
Registrierung neuer Knoten
Um einen Konsensalgorithmus zu implementieren, brauchen wir eine Möglichkeit, einen Knoten über benachbarte Knoten im Netzwerk zu informieren. Jeder Knoten in unserem Netzwerk sollte ein Verzeichnis anderer Knoten im Netzwerk führen. Daher benötigen wir mehr Endpunkte:
- [POST]
/nodes/register
: um eine Liste neuer Knoten in Form von URLs zu akzeptieren. - [GET]
/nodes/resolve
: um unseren Consensus-Algorithmus zu implementieren, der alle Konflikte löst – um sicherzustellen, dass ein Knoten die richtige Kette hat.
Wir müssen den Konstruktor unserer Blockchain ändern und eine Methode zum Registrieren von Knoten bereitstellen:
--- a/src/crystal_coin/blockchain.cr +++ b/src/crystal_coin/blockchain.cr @@ -7,10 +7,12 @@ module CrystalCoin getter chain getter uncommitted_transactions + getter nodes def initialize @chain = [ Block.first ] @uncommitted_transactions = [] of Block::Transaction + @nodes = Set(String).new [] of String end def add_transaction(transaction)
Beachten Sie, dass wir eine Set
-Datenstruktur mit String
-Typ verwendet haben, um die Liste der Knoten zu speichern. Dies ist eine kostengünstige Methode, um sicherzustellen, dass das Hinzufügen neuer Knoten idempotent ist und dass, egal wie oft wir einen bestimmten Knoten hinzufügen, er genau einmal erscheint.
Lassen Sie uns nun ein neues Modul zu Consensus
hinzufügen und die erste Methode register_node(address)
implementieren:
require "uri" module CrystalCoin module Consensus def register_node(address : String) uri = URI.parse(address) node_address = "#{uri.scheme}:://#{uri.host}" node_address = "#{node_address}:#{uri.port}" unless uri.port.nil? @nodes.add(node_address) rescue raise "Invalid URL" end end end
Die Funktion register_node
analysiert die URL des Knotens und formatiert sie.
Und hier erstellen wir den Endpunkt /nodes/register
:
post "/nodes/register" do |env| nodes = env.params.json["nodes"].as(Array) raise "Empty array" if nodes.empty? nodes.each do |node| blockchain.register_node(node.to_s) end "New nodes have been added: #{blockchain.nodes}" end
Bei dieser Implementierung könnten wir nun auf ein Problem mit mehreren Knoten stoßen. Die Kopie von Ketten einiger weniger Knoten kann unterschiedlich sein. In diesem Fall müssen wir uns auf eine Version der Kette einigen, um die Integrität des gesamten Systems aufrechtzuerhalten. Wir müssen einen Konsens erzielen.
Um dies zu lösen, stellen wir die Regel auf, dass die längste gültige Kette verwendet werden soll. Mit diesem Algorithmus erreichen wir einen Konsens zwischen den Knoten in unserem Netzwerk. Der Grund für diesen Ansatz ist, dass die längste Kette eine gute Schätzung des größten Arbeitsaufwands darstellt.
module CrystalCoin module Consensus ... def resolve updated = false @nodes.each do |node| node_chain = parse_chain(node) return unless node_chain.size > @chain.size return unless valid_chain?(node_chain) @chain = node_chain updated = true rescue IO::Timeout puts "Timeout!" end updated end ... end end
Denken Sie daran, dass resolve
eine Methode ist, die alle unsere benachbarten Knoten durchläuft, ihre Ketten herunterlädt und sie mit valid_chain?
Methode. If a valid chain is found, whose length is greater than ours, we replace ours.
Now let's implement parse_chain()
and valid_chain?()
private methods:
module CrystalCoin module Consensus ... private def parse_chain(node : String) node_url = URI.parse("#{node}/chain") node_chain = HTTP::Client.get(node_url) node_chain = JSON.parse(node_chain.body)["chain"].to_json Array(CrystalCoin::Block).from_json(node_chain) end private def valid_chain?(node_chain) previous_hash = "0" node_chain.each do |block| current_block_hash = block.current_hash block.recalculate_hash return false if current_block_hash != block.current_hash return false if previous_hash != block.previous_hash return false if current_block_hash[0..1] != "00" previous_hash = block.current_hash end return true end end end
For parse_chain()
we:
- Issue a
GET
HTTP request usingHTTP::Client.get
to/chain
end-point. - Parse the
/chain
JSON response usingJSON.parse
. - Extract an array of
CrystalCoin::Block
objects from the JSON blob that was returned usingArray(CrystalCoin::Block).from_json(node_chain)
.
There is more than one way of parsing JSON in Crystal. The preferred method is to use Crystal's super-handy JSON.mapping(key_name: Type)
functionality that gives us the following:
- A way to create an instance of that class from a JSON string by running
Class.from_json
. - A way to serialize an instance of that class into a JSON string by running
instance.to_json
. - Getters and setters for keys defined in that class.
In our case, we had to define JSON.mapping
in CrystalCoin::Block
object, and we removed property
usage in the class, like so:
module CrystalCoin class Block JSON.mapping( index: Int32, current_hash: String, nonce: Int32, previous_hash: String, transactions: Array(Transaction), timestamp: Time ) ... end end
Now for Blockchain#valid_chain?
, we iterate through all of the blocks, and for each we:
- Recalculate the hash for the block using
Block#recalculate_hash
and check that the hash of the block is correct:
module CrystalCoin class Block ... def recalculate_hash @nonce = proof_of_work @current_hash = calc_hash_with_nonce(@nonce) end end end
- Check each of the blocks linked correctly with their previous hashes.
- Check the block's hash is valid for the number of zeros (
difficulty
in our case00
).
And finally we implement /nodes/resolve
end-point:
get "/nodes/resolve" do if blockchain.resolve "Successfully updated the chain" else "Current chain is up-to-date" end end
It's done! You can find the final code on GitHub.
The structure of our project should look like this:
crystal_coin [master●] % tree src/ src/ ├── crystal_coin │ ├── block.cr │ ├── blockchain.cr │ ├── consensus.cr │ ├── proof_of_work.cr │ ├── transaction.cr │ └── version.cr ├── crystal_coin.cr └── server.cr
Let's Try it Out
- Grab a different machine, and run different nodes on your network. Or spin up processes using different ports on the same machine. In my case, I created two nodes on my machine, on a different port to have two nodes:
http://localhost:3000
andhttp://localhost:3001
. - Register the second node address to the first node using:
crystal_coin [master●●] % curl -X POST http://0.0.0.0:3000/nodes/register -H "Content-Type: application/json" -d '{"nodes": ["http://0.0.0.0:3001"]}' New nodes have been added: Set{"http://0.0.0.0:3001"}%
- Let's add a transaction to the second node:
crystal_coin [master●●] % curl -X POST http://0.0.0.0:3001/transactions/new -H "Content-Type: application/json" -d '{"from": "eqbal", "to":"spiderman", "amount": 100}' Transaction #<CrystalCoin::Block::Transaction:0x1039c29c0> has been added to the node%
- Let's mine transactions into a block at the second node:
crystal_coin [master●●] % curl http://0.0.0.0:3001/mine Block with index=1 is mined.%
- At this point, the first node has only one block (genesis block), and the second node has two nodes (genesis and the mined block):
crystal_coin [master●●] % curl http://0.0.0.0:3000/chain {"chain":[{"index":0,"current_hash":"00fe9b1014901e3a00f6d8adc6e9d9c1df03344dda84adaeddc8a1c2287fb062","nonce":157,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:45+0300"}]}%
crystal_coin [master●●] % curl http://0.0.0.0:3001/chain {"chain":[{"index":0,"current_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","nonce":147,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:38+0300"},{"index":1,"current_hash":"00441a4d9a4dfbab0b07acd4c7639e53686944953fa3a6c64d2333a008627f7d","nonce":92,"previous_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","transactions":[{"from":"eqbal","to":"spiderman","amount":100}],"timestamp":"2018-05-24T00:23:57+0300"}]}%
- Our goal is to update the chain in the first node to include the newly generated block at the second one. So let's resolve the first node:
crystal_coin [master●●] % curl http://0.0.0.0:3000/nodes/resolve Successfully updated the chain%
Let's check if the chain in the first node has updated:
crystal_coin [master●●] % curl http://0.0.0.0:3000/chain {"chain":[{"index":0,"current_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","nonce":147,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:38+0300"},{"index":1,"current_hash":"00441a4d9a4dfbab0b07acd4c7639e53686944953fa3a6c64d2333a008627f7d","nonce":92,"previous_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","transactions":[{"from":"eqbal","to":"spiderman","amount":100}],"timestamp":"2018-05-24T00:23:57+0300"}]}%
Hurra! Our Crystal language example works like a charm, and I hope you found this lengthy tutorial crystal clear, pardon the pun.
Einpacken
This Crystal language tutorial covered the fundamentals of a public blockchain. If you followed along, you implemented a blockchain from scratch and built a simple application allowing users to share information on the blockchain.
We've made a fairly sized blockchain at this point. Now, CrystalCoin
can be launched on multiple machines to create a network, and real CrystalCoins
can be mined.
I hope that this has inspired you to create something new, or at the very least take a closer look at Crystal programming.
Note: The code in this tutorial is not ready for real-world use. Please refer to this as a general guide only.