De Blockchain in plain Java

Cédric van Beijsterveldt, Johan Kragt

In een vorige uitgave van Java Magazine is een technische introductie gegeven in de blockchain. In dit artikel willen wij hierop verder gaan door aan de hand van een simpele implementatie in Java uit te leggen hoe de belangrijkste componenten in de blockchain werken.

 

Bitcoin en de Blockchain

Als er over de blockchain gesproken wordt gaat het meestal over Bitcoin, de eerste en meest bekende implementatie ervan. Bitcoin is ontworpen als een manier om geld uit te wisselen onder de volgende twee voorwaarden:

  • Er is geen centrale autoriteit
  • Geld mag maar één keer uitgegeven worden

De eerste voorwaarde, namelijk dat er geen centrale autoriteit mag bestaan, is in contrast met hoe bijvoorbeeld een bank werkt. Een bank kan in principe bij elke transactie bepalen of deze wel of niet geaccepteerd wordt, om wat voor reden dan ook. Het doel van Bitcoin is om te zorgen dat deze autoriteit niet door een enkele partij kan worden gegrepen.

De tweede voorwaarde staat ook wel bekend als het ‘double spending’ probleem, en houdt in dat je niet wilt dat je hetzelfde geld meer dan één keer kan uitgeven. Als dit wel toegestaan zou zijn, zou iemand van hetzelfde geld zowel jou als iemand anders gelijktijdig kunnen betalen, waardoor er een conflict ontstaat over wie het bedrag nu daadwerkelijk heeft ontvangen.

 

Onze implementatie

Onze implementatie, zoals besproken in dit artikel, is geschreven in plain Java zonder gebruik te maken van specifieke blockchain libraries of frameworks. We hebben deze implementatie simpel gehouden om ons volledig te kunnen richten op het basale gedrag van de blockchain.

Om snel te kunnen starten en om boilerplate code te reduceren maken we gebruik van Spring Boot en Eureka. Deze frameworks hebben in principe niks te maken met de blockchain zelf en we zullen hier dan ook niet verder op ingaan. We hebben ook een UI toegevoegd waarin we laten zien wat er gebeurt binnen onze blockchain.

Verder zijn er een aantal designkeuzes die je kan maken bij de implementatie van een blockchain. Wij hebben er voor gekozen om het op een vergelijkbare manier als Bitcoin te doen.

 

De Blockchain

De blockchain als geheel is vrij complex en om die reden hebben we besloten om de belangrijkste componenten apart te behandelen. Uiteindelijk zullen deze samenkomen tot een werkende blockchain.

 

  1. Transacties

Transacties liggen aan de basis van de blockchain en zijn uiteindelijk waar het allemaal om draait. De andere componenten zijn er dan ook op gebouwd dit te ondersteunen. Je kent transacties waarschijnlijk van banktransacties, waarbij geld van de ene partij naar de andere wordt verstuurd, en ook Bitcoin is ontworpen voor het versturen van geld. Dit is echter een implementatiedetail, en als we naar de blockchain op zichzelf kijken maakt het eigenlijk niet uit wat we versturen. Een transactie kan dus in principe alles zijn waarbij data of informatie wordt verstuurd. Alle transacties die zijn geaccepteerd worden opgeslagen. In Bitcoin is dat in de vorm van een grootboek (Engels: ledger), dat je kunt zien als een tabel waarin een rij een transactie representeert.

 

In onze implementatie zijn we van een hele simpele transactie uitgegaan, namelijk een bericht met twee velden: een ID en een tekst. We hebben hiervoor een class Message gemaakt:

public class Message {
private final int index;
private final String text;

// Constructors, getters, etc…

// We define messages as being equal if they have the same ID
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || !getClass().equals(o.getClass())) {
return false;
}

final Message message = (Message) o;

return index == message.index;
}
}

Verder geven we één restrictie aan de Messages die in de blockchain worden opgenomen, namelijk dat hun ID’s uniek zijn. Een Message wordt dus alleen maar toegevoegd op het moment dat er nog geen andere bestaat met hetzelfde ID:

 

private final Set<Message> unhandledMessages;
private final Set<Message> handledMessages;

public void addMessage(final Message m) {
// Check whether we already have a message with this ID
if (!handledMessages.contains(m) && !unhandledMessages.contains(m)) {
unhandledMessages.add(m);
}
}
 

  1. Nodes

Nu onze transacties gedefinieerd zijn moeten we iets hebben dat ze kan ontvangen en verder kan behandelen. Dit ‘iets’ noemen we een node.

Een enkele node is in principe genoeg om elke transactie te verwerken, maar dit zou betekenen dat deze ene node een centrale autoriteit is, en dat willen we juist vermijden. De oplossing hiervoor is simpel, namelijk het distribueren van deze autoriteit over een netwerk van meerdere nodes. Daarmee bereiken we een gedistribueerde autoriteit in tegenstelling tot een enkele centrale autoriteit. Hierbij is het belangrijk dat alle nodes in een netwerk gelijk zijn. Elke node kan dan ook volledig individueel zijn werk doen zonder daarbij afhankelijk te zijn van anderen.

In de praktijk is een node verbonden met slechts een handvol andere nodes, die op hun beurt ook weer connecties hebben. Dit zorgt uiteindelijk voor een verspreid netwerk van communicerende nodes. Echter, nogmaals, een node hoeft in principe niet verbonden te zijn met anderen om zijn werk te doen, namelijk het ontvangen en afhandelen van transacties. Een transactie die verstuurd wordt hoeft dus ook niet door elke node ontvangen te worden.

In onze implementatie is elke node een instance van een Spring Boot project met daarin de code waarmee de blockchain zijn werk kan doen. Nodes kunnen elkaar vinden middels een Eureka discovery service. Zoals vermeldt hebben wij in onze implementatie een restrictie op transacties, namelijk dat een transactie pas door een node wordt opgenomen als er nog geen andere bestaat met hetzelfde ID. Het kan dus zijn dat we twee transacties met hetzelfde ID het netwerk insturen, waarbij een aantal nodes de ene als eerste ontvangt en de tweede negeert, en andere nodes vice versa. Het kan ook zijn dat een transactie een bepaalde node nooit bereikt. Zowel het aantal ontvangen als geaccepteerde transacties kan dus tussen nodes gaan verschillen.

 

  1. Blocks

We hebben nu nodes die individueel transacties kunnen verwerken. Als we dit zo zouden houden zouden we een hoop verschillende nodes hebben, elk met hun eigen grootboek aan opgeslagen transacties. Het is dus noodzakelijk dat er onderling gecommuniceerd wordt over de staat van het grootboek.

Voor redenen die in het volgende component, ‘consensus’, verder behandeld worden, is het communiceren van een enkele transactie een traag en duur proces. Als we dit op deze manier zouden willen volhouden, zouden we maar een zeer beperkt aantal transacties per minuut kunnen doen. Dit is niet erg praktisch als je miljoenen mensen over de wereld gebruik wilt laten maken van je blockchain. De oplossing hier is vrij simpel: als het communiceren van enkele transacties te traag is, stuur ze dan in groepen tegelijk!

Dit is in feite wat een block is, namelijk een lijst van verzamelde transacties. Deze blocks worden op hun beurt weer aan elkaar gekoppeld in een soort van ketting, waarbij elk block een referentie heeft naar zijn voorganger. Dit is vergelijkbaar met hoe bijvoorbeeld een single linked list werkt, of een commit in Git. Onze transacties zijn dus uiteindelijk verzameld in een keten van blokken, vandaar de naam blockchain, en elke keer als een node een aantal transacties in een block verzameld informeert deze het netwerk ervan:

 

private void addCreatedBlock(final Block block) {
if (chain.getEndBlockHash().equals(block.getParentHash())) {
chain.addBlock(block);
informNodesOfNewBlock(block);
}
}
 

  1. Consensus

De situatie zoals we die nu hebben bevat nodes die individueel transacties verwerken, deze verzamelen in blocks en dit vervolgens communiceren naar andere nodes. Deze andere nodes zijn echter ook continu met hetzelfde bezig en zullen mogelijk andere transacties hebben binnengekregen. Hierdoor ontstaat er conflict over wat uiteindelijk de waarheid is die geaccepteerd moet worden door het hele netwerk, waarbij de waarheid de geaccepteerde set aan transacties is. De nodes moeten daarom een manier vinden om met elkaar overeen te komen wat de uiteindelijke waarheid zal zijn. Ze moeten, met andere woorden, een staat van consensus bereiken. Dit moet uiteraard door nodes onderling kunnen gebeuren, zonder dat een centrale autoriteit hier een beslissing over neemt.

Er zijn verschillende manieren om tot consensus te komen, maar over het algemeen is in de blockchain geaccepteerd dat de langste ketting altijd wint. Vandaar wordt er in Listing 3 gecontroleerd of het block dat de node heeft gevonden de huidige chain verlengt. Stel dus dat een node A en een node B communiceren, waarbij node A een keten met 10 blocks heeft opgebouwd en node B één met 11. Node A zal zien dat de keten van node B meer blocks heeft dan zichzelf en zal deze, na validatie, accepteren en deze volledige keten, van het eerste block tot het laatste, opnemen als zijn eigen waarheid en hierop verder werken.

Als node A in eerste instantie op een andere chain aan het werk was worden deze blocks na het overnemen van de chain van node B niet verwijderd. De gehele staat van de blockchain kan gezien worden als een boomstructuur en met de verschillende chains als takken. Takken die op een bepaald moment verlaten zijn omdat een andere de langste was blijven bestaan. In theorie is er namelijk kans dat deze ooit nog de langste wordt, al zullen nodes, vanwege de kleine kans, hier over het algemeen niet op doorwerken. In de GUI van onze applicatie worden zulke nodes als ‘orphaned blocks’ aangeduid.

Deze consensus van ‘de langste blockchain wint’ lost echter nog niet alle problemen op. Het creëren van blocks op basis van transacties alleen kan namelijk enorm snel gebeuren. Hierdoor zouden nodes in rap tempo blocks kunnen bouwen en tegen de tijd dat andere nodes hiervan op de hoogte zijn, zijn deze zelf mogelijk al verder en is de informatie van de node die het bericht oorspronkelijk stuurde alweer achterhaald. Voor dit probleem zijn meerdere oplossingen te bedenken. In onze implementatie hebben we gekozen voor de oplossing die in Bitcoin is opgenomen, namelijk een Proof of Work.

 

  1. Proof of Work

Een Proof of Work brengt een extra restrictie aan het creëren van een block, waardoor dit proces moeilijker wordt en meer tijd in beslag neemt. Over het algemeen is het iets dat veel rekenkracht vraagt en het representeert daarmee een investering in de blockchain. Hoewel de berekening lastig is, is de verificatie ervan heel snel te doen, waardoor andere nodes snel kunnen nagaan of een blockchain valide is. Dit proces van het vinden van een nieuw block wordt ook wel ‘mining’ genoemd, en in het geval van bijvoorbeeld Bitcoin wordt de vinder ervan beloond met een aantal Bitcoins. Bij minen ben je dus niet op zoek naar Bitcoins zelf, maar probeer je een block te vinden zodat je er een aantal als beloning krijgt.

Hoe werkt zo’n Proof of Work dan? Hier zijn, wederom, meerdere opties mogelijk. In het geval van Bitcoin gaat het om het berekenen van een hash die voldoet aan een bepaalde conditie. Dit gaat als volgt:

  • Er wordt een conditie gesteld voor een bepaalde hash, in het geval van Bitcoin is dit een hash met bepaald aantal leading ‘0’s, bijvoorbeeld 17.
  • Alle nodes die meedoen aan het minen proberen zo’n hash te vinden. Dit doen ze door een hash samen te stellen op basis van de transacties die ze in het block willen stoppen en een willekeurig getal genaamd een nonce.
  • Voor elke berekende hash wordt gekeken of deze voldoet aan de gestelde conditie. Als dit niet het geval is wordt de nonce verandert en wordt er daarmee een nieuwe hash berekend.
  • Zodra een node een valide hash berekend heeft creëert het een block waarin deze oplossing staat en voegt het dit toe aan zijn eigen chain. Vervolgens notificeert het de nodes waarmee het verbonden is.
  • Andere nodes valideren de Proof of Work en nemen, als de chain van de node waarvan ze het bericht ontvangen langer is dan hun eigen, die volledige blockchain over als hun eigen waarheid.

 

We hebben zelf een hash conditie gemaakt waarin de letters van ons bedrijf, JCore, in die volgende in de berekende hash moeten voorkomen, met een willekeurig aantal karakters ertussen:

 

public boolean isValidHash(final String hash) {
if (null == hash) {
return false;
}

final int jIndex = hash.indexOf(‘J’);
final int cIndex = hash.indexOf(‘C’);
final int oIndex = hash.indexOf(‘o’);
final int rIndex = hash.indexOf(‘r’);
final int eIndex = hash.indexOf(‘e’);

final boolean didFindJcoreCharacters = jIndex != -1 && cIndex != -1 && oIndex != -1 && rIndex != -1 && eIndex != -1;
final boolean jcoreCharactersInCorrectOrder = jIndex < cIndex && cIndex < oIndex && oIndex < rIndex && rIndex < eIndex;

return didFindJcoreCharacters && jcoreCharactersInCorrectOrder;
}
 

Met deze hashing conditie komen we tot de code voor het creëren van een block zoals in Listing 5, waarbij we een Thread.sleep gebruiken om extra vertraging over onze berekening te krijgen.

 

public Block createBlock(Block parentBlock, Set messages) {
String hash;
String parentHash = parentBlock.getHash();
long nonce = random.nextLong();
do {
hash = jChainHasher.hash(parentHash, messages, Long.toString(++nonce));
try {
// Don’t try this at home!
Thread.sleep(parameterService.getDifficulty());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} while (!jChainHasher.isValidHash(hash) && state == RUNNING);

return new Block(hash, parentHash, messages, Long.toString(nonce));
}

 

De bovenstaande manier voor het bereiken van consensus heeft een aantal interessante consequenties. We hebben namelijk gezegd dat de langste blockchain wint en dat blocks alleen gemaakt kunnen worden met een valide Proof of Work. We hebben echter nooit gezegd dat een block permanent is zodra deze is opgenomen door de rest van het netwerk. Het is theoretisch mogelijk om een block op een bepaald punt in de chain te pakken en hierop zelf verder te gaan en, zodra deze langer is dan de geaccepteerde blockchain in de rest van het netwerk, los te laten. Andere nodes zullen zich houden aan de gestelde eisen en de nieuwe blockchain accepteren als deze langer en valide is. Het is dus op deze manier mogelijk om transacties die in eerste instantie geaccepteerd waren te overschrijven. Dit is mogelijk op het moment dat je sneller nieuwe blocks kan creëren dan alle andere nodes, wat kan zijn doordat je meer rekenkracht hebt (dit wordt een 51% attack genoemd) of omdat je het hashing algoritme gekraakt hebt waardoor je beter kan raden wat voor nonce je nodig hebt voor een valide Proof of Work. De distributie van resources over het netwerk en een veilig hashing algoritme zijn essentieel in het voorkomen hiervan, en zolang deze omstandigheden blijven kan een implementatie als Bitcoin veilig werken.

 

Conclusie

We hebben met dit artikel geprobeerd om de blockchain, of in ieder geval één mogelijke implementatie, zo duidelijk mogelijk uit te leggen. Er zijn uiteraard meerdere implementaties mogelijk, maar dit geeft een indicatie over wat de basis van een blockchain is en hoe deze in essentie werkt.

 

Onze code is online beschikbaar op https://github.com/jcoreNL/Chain.