· 

BLOCKCHAIN in PYTHON IMPLEMENTIEREN

In meinem Video zur Einführung in das Thema Blockchain hast du gelernt, was man unter einer Hash-Funktion versteht, wie eine Blockchain aufgebaut werden kann und welchen Sinn sie überhaupt hat. Nun ist es an der Zeit, dieses Wissen einmal in der Praxis umzusetzen. Dazu entwickeln wir eine eigene kleine Blockchain in Python und speichern darin exemplarisch ein paar Transaktionen. Zusätzlich implementieren wir einen Mechanismus, der Manipulationsversuche an unserer Blockchain aufdeckt und testen diesen selbstverständlich auch.

Bevor du dir diesen Artikel durchliest, solltest du dir das bereits erwähnte Theorievideo zur Blockchain anschauen:


Implementierung der Klasse <Block>

Zunächst implementieren wir uns eine Klasse Block, die quasi die Kettenglieder unserer Blockchain darstellen. Zum Hashen der Einträge benötigen wir die Bibliothek hashlib, die wir uns einfach importieren.

import hashlib

class Block:

Danach definieren wir einen Konstruktor, der eine Transaktionsnummer, den eigentlichen Inhalt und den Hash des vorherigen Blocks erhält.

   def __init__(self, nr, data, p_hash):

Diese Übergabeparameter weisen wir Objektvariablen zu. 

      self.nr = nr
      self.data = data
      self.p_hash = p_hash

Der Hash unseres Blocks wird nun dadurch gebildet, dass wir die Transaktionsnummer, den Inhalt und den Hash des vorherigen Blocks konkatenieren. 

Durch diesen Hashwert wird unser Block beim Hashen eindeutig. Zusätzlich könnte man noch weitere Metadaten wie bspw. einen  Timestamp hinzufügen, doch um das Programm nicht unnötig zu überfrachten, belassen wir es erstmal bei diesen Informationen. Wir encoden den zusammengesetzten Inhalt, hashen ihn mit der Funktion sha256 auf hashlib und wiesen den Hexdigest davon unserer Variable zu.

      self.hash = hashlib.sha256((str(nr) + data + p_hash).encode()).hexdigest()

Nun erzeugen wir uns noch eine menschenlesbare Darstellung unseres Blocks. Dazu überladen wir die magische Methode __str__. Als Format wählen wir die folgende Darstellung:

   def __str__(self):
      return '{}:::{};{}'.format(self.nr, self.hash, self.data)

Ganz vorne steht unsre Transaktionsnummer, danach folgt der Hash des Blocks und zum Schluss kommt dann der Inhalt. Die schlussendliche Darstellung bleibt natürlich dem Entwickler der Blockchain überlassen.


Implementierung der Klasse <BlockChain>

Jetzt definieren wir uns eine Klasse BlockChain

class BlockChain():

Im Konstruktor wird ein Genesis-Block erzeugt. Du erinnerst dich: Das war das erste Glied einer BlockChain, die logischerweise keinen Vorgänger hat. Deshalb erhält dieses Objekt vom Typ Block (diesen Typen wir uns gerade eben definiert) die Transaktionsnummer 0, den Inhalt "GENESIS" und als Pseudohashwert 64 mal die 0. Warum 64 mal? Nun, der Hexdigest des SHA256 Hashes ist 64 Stellen lang. 

   def __init__(self):
      genesis_block = Block(0, 'GENESIS', '0'*64)

Im Konstruktor wird zusätzlich noch eine Liste für die einzelnen Blöcke, die unserer Blockchain noch hinzugefügt werden, definiert. Am Anfang enthält diese Liste natürlich nur den Genesis-Block.

      self.blocks = [genesis_block]

Um weitere Blöcke unserer Blockchain hinzufügen zu können, definieren wir uns eine Methode append. Diese erhält als Übergabeparameter die Daten, die es in der Blockchain zu speichern gilt. 

   def append(self, data):

Da wir im Konstruktor bereits die bereits eine Objektvariable blocks zum Sammeln der Blöcke vorgesehen haben, die vom Typ her eine Liste ist, rufen wir hierauf die in der Listenklasse zur Verfügung gestellte Methode append auf. 

      self.blocks.append(

Dort bauen wir uns auf Basis der bisherigen Informationen einen validen Block zusammen, der die zu speichernden Daten kapselt. Als Transaktionsnummer verwenden wir die aktuelle Anzahl an Blöcken in unserer blocks-Liste. Da sich am Anfang darin direkt der Genesis-Block befindet, erhält die erste richtige Transaktion die Nummer 1. Danach folgen die Daten und der Hashwert des vorherigen Blocks. Dazu ziehen wir von der Anzahl der bisher gespeicherten Blöcke 1 ab und holen uns durch direkten Zugriff auf die Objektvariable den Hashwert des jeweiligen Blocks. 

         Block(
            len(self.blocks),
            data,
            self.blocks[len(self.blocks)-1].hash
        )
      )

Jetzt überladen wir wieder die magische Methode __str__, um eine schöne lesbare Darstellung unsrer Blockchain bereitzustellen. 

   def __str__(self):

Zu Beginn definieren wir dafür eine result-Variable, die mit einem leeren String initialisiert wird. 

      res = ''

Jetzt iterieren wir über alle Blöcke unserer Blockchain und speichern in der result-Variable die String-Darstellung eines Blocks, ergänzt um einen Zeilenumbruch. Wir nutzen die Methode format, da wir so implizit die __str__ Methode aufrufen und die Blockdarstellung an unsere Darstellung anhängen.

      for block in self.blocks:
         res += '{}\n'.format(block)

Zum Schluss geben wir die so erzeugte String-Darstellung zurück.

      return res

Testen der Blockchain

Jetzt sind wir in der Lage, unsere Blockchain einmal zu testen. Dazu erzeugen wir uns ein Blockchain-Objekt und fügen ein paar Transaktionen hinzu.

  • Zu Beginn sendet Asuna an Kirito 0.002 BTC.
  • Hitagi sendet Mayuri 0.087 BTC.
  • Mamoru sendet 1.2 BTC an Usagi.
  • Da Usagi Schulden bei ihrer Freundin Asuna hat, sendet sie die Hälfte ihrer empfangenen Bitcoin (also 0.6 BTC) an Asuna.
bc = BlockChain()
bc.append('<Asuna> sendet <Kazuto> <0.002 BTC>')
bc.append('<Hitagi> sendet <Mayuri> <0.087 BTC>')
bc.append('<Mamoru> sendet <Usagi> <1.2 BTC>')
bc.append('<Usagi> sendet <Asuna> <0.6 BTC>')

Zum Schluss drucken wir unsere Blockchain durch einen Aufruf von print auf die Konsole. 

print(bc)

Ist unsere Blockchain valide?

Wie heißt es doch so schön? Vertrauen ist gut, Kontrolle ist besser. Deshalb implementieren wir uns noch eine Methode validate, die mit der überprüft werden soll, ob unsere Blockchain tatsächlich auch valide und frei von Manipulationen ist. 

   def validate(self):

Wie könnte man denn eine Manipulation entdecken? Nun, z. B. anhand der Transaktionsnummer. In unserem Modell ist sie fortlaufend. Wenn wir also eine Transaktionsnummer löschen oder eine hinzufügen, dann liegt wahrscheinlich eine Manipulation vor. 

Es könnte aber auch sein, dass der Hash des vorherigen Blocks nicht mit dem übereinstimmt, der im aktuellen Block als Hash des vorherigen Blocks abgespeichert ist. Um diese Checks programmatisch abzubilden, laufen wir nacheinander alle Blöcke unserer Blockchain ab und zwar beginnend ab dem ersten Block nach unserem Genesis-Block. 

      for i in range(1, len(self.blocks)):

Wenn die Transaktionsnummer des aktuellen Blocks nicht dem Laufindex i entspricht, dann wurde eine Manipulation an der Transaktionsnummer durchgeführt und wir geben False zurück. 

         if self.blocks[i].nr != i:
            print('Manipulation der TAC in Block {} entdeckt!'.format(i))
            return False

Gleiches gilt übrigens auch, wenn der Hash des vorherigen Blocks nicht dem Wert im aktuell betrachteten Block entspricht.

         if self.blocks[i-1].hash != self.blocks[i].p_hash:
            print('Manipulation der Daten in Block {} entdeckt!'.format(i-1))
            return False

Wenn bis zum Ende keine Manipulation festgestellt wurde, teilen wir das dem Anwender mit und geben True zurück. 

      print('Blockchain valide!')
      return True

Das soll uns an dieser Stelle erstmal reichen. Man könnte (falls vorhanden) auch noch den Timestamp überprüfen, doch darauf verzichten wir an dieser Stelle. Das kannst du als kleine Hausaufgabe mal selbst umsetzen.


Manipulation der Blockchain

So, dann wenden wir diese Methode doch einmal direkt an. Zunächst schauen wir in unserer bereits definierten Blockchain, ob diese keinen Manipulationen ausgesetzt war. Dazu rufen wir auf ihr die Methode validate auf.

bc.validate()

Jetzt nehmen wir mal eine Manipulation an Block 3 vor. Dazu tragen wir in die Objektvariable blocks einen gefälschten Block ein. Usagi möchte nämlich nicht die Hälfte der von Mamoru erhaltenen Bitcoin an ihre Freundin abgeben müssen. Deshalb beschließt sie, sich 0.3 BTC mehr von Mamoru zu holen. Dadurch ändert sich aber natürlich automatisch der Hashwert von Block 3

bc.blocks[3] = Block(3, '<Mamoru> sendet <Usagi> <1.5 BTC>', bc.blocks[2].p_hash)

Wenn Usagi in ihren manipulierten Block jetzt den Hash des vorherigen Blocks einträgt, wird die Manipulation in Block 4 erkannt. 

Es bedarf einer Reihe weiterer Maßnahmen, um den Betrug zu entdecken. Eine Möglichkeit würde z.B. darin bestehen, den in meinem Theorievideo zur Blockchain erwähnten Proof-of-Work-Mechanismus umzusetzen. Das könntest du ganz simpel simulieren, indem du beim Erzeugen des Blocks im Konstruktor ein sleep einbaust. Dafür importierst du dir neben hashlib noch time und rufst bspw. 

time.sleep(600)

für 10 Minuten Wartezeit auf. 

   def __init__(self, nr, data, p_hash):
      self.nr = nr
      self.data = data
      self.p_hash = p_hash
      time.sleep(600)
      self.hash = hashlib.sha256((str(nr) + data + p_hash).encode()).hexdigest()

Zudem könntest du ein P2P-Netzwerk aus mehreren Teilnehmern aufbauen, die jeweils eine Kopie des aktuellen Stands der Blockchain haben und so Manipulationen erkennen können. Das umzusetzen würde jedoch den Rahmen dieses bewusst primitiv gehaltenen Blockchain-Modells sprengen. Du hast jetzt aber zumindest eine Idee, wie die in meinem Video zur Einführung in das Thema Blockchain besprochenen Grundlagen in der Praxis umgesetzt werden können.


Quellcode zum Download

Hier kannst du dir den gesamten Quellcode des Python-Programms kostenlos herunterladen bzw. kopieren.

import hashlib

class Block:
   def __init__(self, nr, data, p_hash):
      self.nr = nr
      self.data = data
      self.p_hash = p_hash
      self.hash = hashlib.sha256((str(nr) + data + p_hash).encode()).hexdigest()
        
   def __str__(self):
      return '{}:::{};{}'.format(self.nr, self.hash, self.data)
        
class BlockChain():
   def __init__(self):
      genesis_block = Block(0, 'GENESIS', '0'*64)
      self.blocks = [genesis_block]
        
   def append(self, data):
      self.blocks.append(
         Block(
            len(self.blocks),
            data,
            self.blocks[len(self.blocks)-1].hash
        )
      )
        
   def __str__(self):
      res = ''
      for block in self.blocks:
         res += '{}\n'.format(block)
      return res
        
   def validate(self):
      for i in range(1, len(self.blocks)):
         if self.blocks[i].nr != i:
            print('Manipulation der TAC in Block {} entdeckt!'.format(i))
            return False
         if self.blocks[i-1].hash != self.blocks[i].p_hash:
            print('Manipulation der Daten in Block {} entdeckt!'.format(i-1))
            return False
      print('Blockchain valide!')
      return True
        
bc = BlockChain()
bc.append('<Asuna> sendet <Kazuto> <0.002 BTC>')
bc.append('<Hitagi> sendet <Mayuri> <0.087 BTC>')
bc.append('<Mamoru> sendet <Usagi> <1.2 BTC>')
bc.append('<Usagi> sendet <Asuna> <0.6 BTC>')

bc.blocks[3] = Block(3, '<Mamoru> sendet <Usagi> <1.5 BTC>', bc.blocks[2].p_hash)

print(bc)
bc.validate()