Die Blockchain in 9 Schritten – Teil 7: Vorbereitung des JavaCryptoCoin
Diese Seite ist anders als die übrigen Kapitel dieser Reihe, denn es gibt kein lauffähiges Programm und auch keine Konsolenausgabe. Da aber der Schritt von Teil 6 zum funktionsfähigen elektronischen Geld eine Menge Erläuterungen bedarf, teile ich die Informationen auf zwei Teile auf.
Unsere elektronische Währung wird in einer Geldbörse, dem „Wallet“, aufbewahrt. Leider verschwindet das Geld und das Wallet sobald unser Programm beendet wird, daher kümmere ich mich jetzt um eine Speichermöglichkeit. Zum Glück sind alle Daten in eigenen Klassen strukturiert, so das die Speicherung mit minimalem Aufwand programmiert werden kann. Ich nutze einen „FileOutputStream“ in Verbindung mit einem „ObjektOutputStream“, um die Daten komplett auf die Festplatte zu schreiben und mit den „Input-Stream“-Varianten wieder einzulesen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
package net.bplaced.javacrypto.blockchain.step8; /* * Diese Klasse gehört zu JavaCryptoCoin.java * This class belongs to JavaCryptoCoin.java */ import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class FileUtil { public static void saveObject(String filenameString, Object object) { try { FileOutputStream fo = new FileOutputStream(new File(filenameString)); ObjectOutputStream oo = new ObjectOutputStream(fo); oo.writeObject(object); oo.close(); fo.close(); } catch (FileNotFoundException e) { System.out.println("Datei nicht gefunden"); } catch (IOException e) { System.out.println("Fehler bei der Initialisierung des Streams"); } } public static Object loadObject(String filenameString) { Object object = null; try { FileInputStream fi = new FileInputStream(new File(filenameString)); ObjectInputStream oi = new ObjectInputStream(fi); object = (Object) oi.readObject(); oi.close(); fi.close(); } catch (FileNotFoundException e) { System.out.println("Datei nicht gefunden"); } catch (IOException e) { System.out.println("Fehler bei der Initialisierung des Streams"); } catch (ClassNotFoundException e) { e.printStackTrace(); } return object; } public static void saveJsonFile(String filenameString, String jsonString) throws IOException { BufferedWriter writer = new BufferedWriter(new FileWriter(filenameString)); writer.write(jsonString); writer.close(); } } |
Damit die Auslagerung auf diese Weise funktioniert benötigen unsere Klassen noch etwas Vorbereitung, wie wir an der veränderten Block-Klasse sehen. Die Klasse importiert und implementiert „Serializable“ und hat eine (von Eclipse generierte) eindeutige Seriennummer. Weiterhin ist die Zahl der Datenfelder deutlich erhöht und wir nehmen die Ergänzung einer Transaktion mit in die Klasse auf:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
package net.bplaced.javacrypto.blockchain.step8; /* * Diese Klasse gehört zu JavaCryptoCoin.java * This class belongs to JavaCryptoCoin.java */ import java.io.Serializable; import java.util.ArrayList; import java.util.Date; public class Block implements Serializable { private static final long serialVersionUID = 1984888774694344646L; public String hash; public String previousHash; public String merkleRoot; public ArrayList<Transaction> transactions = new ArrayList<>(); public long timeStamp; public int nonce; public long mineStartTimeStamp; public long mineEndTimeStamp; public String difficultyString = ""; public boolean mined = false; public Block(String previousHash) { this.previousHash = previousHash; this.timeStamp = new Date().getTime(); this.hash = calculateHash(); } public String calculateHash() { String calculatedhash = StringUtil.generateSha256( previousHash + Long.toString(timeStamp) + Integer.toString(nonce) + merkleRoot + Long.toString(mineStartTimeStamp) ); return calculatedhash; } public void mineBlock(int difficulty) { mineStartTimeStamp = new Date().getTime(); merkleRoot = StringUtil.getMerkleRoot(transactions); String target = StringUtil.getDifficultyString(difficulty, merkleRoot); while (!hash.substring(0, difficulty).equals(target)) { nonce++; hash = calculateHash(); } mineEndTimeStamp = new Date().getTime(); difficultyString = target; mined = true; System.out.println("Block Mined!!! " + (mineEndTimeStamp - mineStartTimeStamp) + " ms: " + hash); } public boolean addTransaction(Transaction transaction) { if (transaction == null) { return false; } if ((!"0".equals(previousHash)) && !transaction.asReward) { if ((transaction.processTransaction() != true)) { System.out.println("Die Transaktion konnte nicht bearbeitet werden, der Vorgang wurde abgebrochen."); return false; } } transactions.add(transaction); System.out.println("Die Transaktion wurde dem Block erfolgreich hinzugefügt"); return true; } } |
Unsere StringUtil-Klasse wird geringfügig ergänzt, damit wir alle String relevanten Funktionen in der Klasse vereinen können:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
package net.bplaced.javacrypto.blockchain.step8; /* * Diese Klasse gehört zu JavaCryptoCoin.java * This class belongs to JavaCryptoCoin.java */ import java.security.*; import java.util.ArrayList; import java.util.Base64; import com.google.gson.GsonBuilder; import java.io.UnsupportedEncodingException; import java.util.List; public class StringUtil { public static String generateSha256(String input) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hash = digest.digest(input.getBytes("UTF-8")); StringBuilder hexString = new StringBuilder(); for (int i = 0; i < hash.length; i++) { String hex = Integer.toHexString(0xff & hash[i]); if (hex.length() == 1) { hexString.append('0'); } hexString.append(hex); } return hexString.toString(); } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) { throw new RuntimeException(e); } } public static byte[] generateECDSASig(PrivateKey privateKey, String input) { Signature dsa; byte[] output = new byte[0]; try { dsa = Signature.getInstance("SHA256withECDSA"); dsa.initSign(privateKey); byte[] strByte = input.getBytes(); dsa.update(strByte); byte[] realSig = dsa.sign(); output = realSig; } catch (InvalidKeyException | NoSuchAlgorithmException | SignatureException e) { throw new RuntimeException(e); } return output; } public static boolean verifyECDSASig(PublicKey publicKey, String data, byte[] signature) { try { Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA"); ecdsaVerify.initVerify(publicKey); ecdsaVerify.update(data.getBytes()); return ecdsaVerify.verify(signature); } catch (InvalidKeyException | NoSuchAlgorithmException | SignatureException e) { throw new RuntimeException(e); } } public static String getJson(Object o) { return new GsonBuilder().setPrettyPrinting().create().toJson(o); } public static String getDifficultyString(int difficulty, String merkleRoot) { String s = ""; for(char c : merkleRoot.toCharArray()) { if(Character.isDigit(c)) s = s + c; } if(s.equals("") || s.length() <= 5 ) s = "0123456789"; String ret = ""; for(int i = 1; i <= difficulty; i++) { int n = difficulty * i; while(n >= s.length()) { n = (n - s.length()) + i; } if(n < 0) n = 0; ret = ret + s.substring(n, n + 1); } return ret; } public static String getStringFromKey(Key key) { return Base64.getEncoder().encodeToString(key.getEncoded()); } public static String getMerkleRoot(ArrayList<Transaction> transactions) { int count = transactions.size(); List<String> previousTreeLayer = new ArrayList<>(); for (Transaction transaction : transactions) { previousTreeLayer.add(transaction.transactionId); } List<String> treeLayer = previousTreeLayer; while (count > 1) { treeLayer = new ArrayList<>(); for (int i = 1; i < previousTreeLayer.size(); i += 2) { treeLayer.add(generateSha256(previousTreeLayer.get(i - 1) + previousTreeLayer.get(i))); } count = treeLayer.size(); previousTreeLayer = treeLayer; } String merkleRoot = (treeLayer.size() == 1) ? treeLayer.get(0) : ""; return merkleRoot; } } |
Die letzte Klasse in diesem Teil ist unsere Wallet-Klasse, welches zwei neue Methoden erhalten hat. Zum Einen wird der aktuelle „Kontostand“ oder Inhalt der Geldbörse berechnet, zum zweiten kann ich nun meine neue Währung von diesem Wallet in eine andere Geldbörse übertragen. Eine kleine Erläuterung noch zur Errechnung des Kontostandes: Die Blockchain-Technologie zeichnet sich gerade dadurch aus, das es keine zentrale Verwaltung oder auch „Bank“ gibt, sondern das alle Transaktionen in der Blockchain gespeichert werden und die Blockchain auf viele Rechner dupliziert wird. Damit kann eine neue Transaktion an verschiedenen Orten „zuerst“ auftauchen und später durch die Synchronisation auch mich und meine Geldbörse erreichen. Damit gibt es aber keine „richtige“ Geldbörse, bei der ein Blick in Portemonnaie ausreicht. Zur Ermittlung meines „Kontostandes“ bin ich darauf angewiesen, stets die komplette Blockchain auf Transaktionen durchzusehen, die mein Wallet betreffen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
package net.bplaced.javacrypto.blockchain.step8; /* * Diese Klasse gehört zu JavaCryptoCoin.java * This class belongs to JavaCryptoCoin.java */ import java.io.Serializable; import java.security.*; import java.security.spec.ECGenParameterSpec; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; public class Wallet implements Serializable{ private static final long serialVersionUID = 3090822928966119493L; public PrivateKey privateKey; public PublicKey publicKey; public HashMap<String, TransactionOutput> UTXOs = new HashMap<>(); public Wallet() { generateKeyPair(); } public void generateKeyPair() { try { // secp192r1 [NIST P-192, X9.62 prime192v1] (1.2.840.10045.3.1.1) // secp224k1 (1.3.132.0.32) // secp224r1 [NIST P-224] (1.3.132.0.33) // secp256k1 (1.3.132.0.10) // secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7) KeyPairGenerator keypairGenerator = KeyPairGenerator.getInstance("EC", "SunEC"); // ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp192r1"); ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1"); keypairGenerator.initialize(ecSpec, new SecureRandom()); KeyPair keyPair = keypairGenerator.genKeyPair(); privateKey = keyPair.getPrivate(); publicKey = keyPair.getPublic(); } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchProviderException e) { throw new RuntimeException(e); } } public float getBalance() { float total = 0; for (Map.Entry<String, TransactionOutput> item : JavaCryptoCoin.UTXOs.entrySet()) { TransactionOutput UTXO = item.getValue(); if (UTXO.isMine(publicKey)) { //if output belongs to me ( if coins belong to me ) UTXOs.put(UTXO.id, UTXO); //add it to our list of unspent transactions. total += UTXO.value; } } return total; } public Transaction sendFunds(PublicKey _recipient, float value) { if (getBalance() < value) { System.out.println("# Das Guthaben ist zu gering um die Transaktion auszuführen, die Transaktion wurde abgebrochen."); System.out.println("# Diese Transaktion scheitert: Empfänger:" + _recipient + "\n Überweisungsbetrag:" + value); return null; } ArrayList<TransactionInput> inputs = new ArrayList<>(); float total = 0; for (Map.Entry<String, TransactionOutput> item : UTXOs.entrySet()) { TransactionOutput UTXO = item.getValue(); total += UTXO.value; inputs.add(new TransactionInput(UTXO.id)); if (total > value) { break; } } Transaction newTransaction = new Transaction(publicKey, _recipient, value, inputs); newTransaction.generateSignature(privateKey); for (TransactionInput input : inputs) { UTXOs.remove(input.transactionOutputId); } return newTransaction; } } |
Weiter geht es mit Teil 8: I08 Der JavaCryptoCoin.
Alle Quellcodes zur Blockchain findet Ihr zum Download in meinem Github-Repository, welches Ihr über diesen Link erreicht: https://github.com/java-crypto/I-Blockchain. Alle Programme sind sowohl unter Java 8 als auch unter Java 11 lauffähig.
Die Lizenz zum obigen Beispiel findet Ihr auf der eigenen Lizenz-Seite.
Letzte Aktualisierung: 12.06.2019