Byg et sæt blockchains ved hjælp af Java PlatoBlockchain Data Intelligence. Lodret søgning. Ai.

Byg et sæt blockchains ved hjælp af Java

Tillid via peer-to-peer-netværk

edderkoppespind
Billede af ClaudiaWollesen fra Pixabay

I den første del af denne serie lavede vi en enkelt blockchain. Nu skal vi lave et sæt af dem og få dem til at tale med hinanden. Det egentlige punkt med blockchain er et distribueret verifikationssystem. Du kan tilføje blokke fra alle noder, og til sidst kommer den til peer noder, så alle er enige om, hvordan blockchain ser ud. Dette er vigtigt, fordi du ikke ønsker en enkelt kilde til sandhed i et distribueret system.

Der er et problem, der dukker op med det samme: Hver node er to tjenester, plus en MongoDB og en Kafka-meddelelsesbus, der alle skal tale med hinanden. Men jeg vil teste og demonstrere flere noder på en enkelt vært (min bærbare computer). Jeg har kørt med Docker compose, så jeg bliver nødt til at lave én Docker compose-fil for hver node for at holde portene lige.

Vi vil arbejde på en nodetjeneste, der gør det muligt for noderne at arbejde med hinanden. Dette vil få input fra to steder, en afslappende grænseflade, der giver dig mulighed for at tilføje og liste de tilsluttede noder, og en meddelelsesbus leveret af Kafka, der underretter nodetjenesten om ændringer i den lokale blockchain, der skal udsendes til peer-noder.

For at tjenesterne kan bruges som billeder, vil jeg bruge Google Jib maven plugin. Dette er den enkleste måde at skabe et billede fra en maven build. Vi tilføjer følgende til pom-filen for hvert modul, som vi skal bruge et billede til:




com.google.cloud.tools
jib-maven-plugin
2.7.1


I vores tilfælde kan vi bruge standardværdier til konfigurationen. Dernæst kan du løbe mvn clean install jib:build og det vil skabe et billede, du kan bruge i din Docker-komponeringsfil.

Lad os tage det, vi har indtil videre og starte det hele op med en altomfattende Docker-komponeringsfil kaldet docker-compose-node1:

version: '3.1'
tjenester:
mongo:
billede: mongo
genstart: altid
porte:
- 27017
miljø:
MONGO_INITDB_ROOT_USERNAME: rod
MONGO_INITDB_ROOT_PASSWORD: eksempel
mongo-ekspress:
billede: mongo-express
genstart: altid
porte:
8081
miljø:
ME_CONFIG_MONGODB_ADMINUSERNAME: root
ME_CONFIG_MONGODB_ADMINPASSWORD: eksempel
dyrepasser:
billede: confluentinc/cp-zookeeper:nyeste
porte:
- 2181
miljø:
ZOOKEEPER_CLIENT_PORT: 2181
kafka:
billede: confluentinc/cp-kafka:nyeste
porte:
- 9092
- 29092
venstre:
- dyrepasser
miljø:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: dyrepasser:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
blockchain:
billede: rlkamradt/blockchain:1.0-SNAPSHOT
porte:
8080
miljø:
MONGO_HOST: mongo
SPRING_KAFKA_BOOTSTRAP-SERVERE: kafka:29092

Du vil bemærke, at et par ting har ændret sig. Først fjernede jeg den eksterne port fra Kafka, så den kun vil være tilgængelig inde i compose-netværket. Så har jeg tilføjet blockchain-appen med det billede, der er skabt af buildet. Til sidst tilsidesatte jeg et par fjederegenskaber med miljøvariablen, så den får adgang til mongo og Kafka inde fra compose-netværket. Compose vil oprette DNS-poster, så tjenester, der kører fra den samme skrivefil, kan få adgang til hinanden. Kør det med denne kommando docker compose -f docker-compose-node1.yaml up -d og sørg for, at du stadig kan ramme den grundlæggende blockchain API:

Nu er vores applikation oppe og køre og har skabt genesis-blokken.

Der er allerede kode i blockchain-tjenesten, der sender en besked til Kafka, hver gang vi tilføjer en blok eller tilføjer en transaktion. Vi er nødt til at oprette en ny tjeneste, der læser disse begivenheder og udsender dem til en liste over jævnaldrende. Lad os nedbringe det, vi kører for nu, og tilføje en simpel nodetjeneste, der vil logge en besked, når den er modtaget. Vi skal bruge et nyt modul i projektet - dette vil være endnu en Spring Boot-tjeneste, og den vil være i stand til at tale med eksterne noder.

Først skal vi definere en node, som kun er en URL. Her er Node:

@Data
@Bygger
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = sand)
@Dokument
offentlig klasse Node {
@Id
privat streng-id;
privat streng url;
}

Vi gemmer det i en samling i MongoDB, så vi har brug for en lagergrænseflade:

@Repository
offentlige grænseflade NodeRepository udvider
ReactiveMongoRepository {
Mono findByUrl(String url);
}

Dernæst skal vi have en controller for at kunne se og tilføje Node objekter:

@Slf4j
@RestController
@RequestMapping("/node")
public class SimpleNodeController {
private endelige Blockchain blockchain;
privat endelig SimpleNodeService simpleNodeService;

offentlig SimpleNodeController(Blockchain blockchain,
SimpleNodeService simpleNodeService) {
this.simpleNodeService = simpleNodeService;
this.blockchain = blockchain;
}

@GetMapping(sti = "peers", producerer =
Medietype.TEXT_EVENT_STREAM_VALUE)
Strøm getNodes() {
returner simpleNodeService.getPeers();
}

@PostMapping(sti = "peers", producerer =
Medietype.APPLICATION_JSON_VALUE)
Mono addNode(@RequestBody Node node) {
returner simpleNodeService.connectToPeer(node);
}

}

Så har vi brug for en simpel serviceklasse, der vil interagere med MongoDB og Kafka:

@Komponent
@Slf4j
public class SimpleNodeService {
privat statisk node mig selv;
endelig privat streng vært;
endelige private int port;
endelige private NodeRepository peers;
endelige private Blockchain blockchain;
endelig privat ReactiveKafkaConsumerTemplate
Besked> sender;

public SimpleNodeService(@Value("${server.host}") Streng vært,
@Value("${server.port}") Strengport,
Blockchain blockchain,
NodeRepository peers,
ReactiveKafka Consumer Template emitter) {
this.host = vært;
this.port = Heltal.parseInt(Havn);
this.blockchain = blockchain;
this.emitter = emitter;
this.peers = jævnaldrende;
mig selv = Node.Builder()
.url("http://" + host + ":" + port)
.build ();
emitter
.receiveAutoAck()
.doOnNext(consumerRecord -> log.info(
"modtaget nøgle={}, værdi={} fra emne={}, offset={}",
consumerRecord.key(),
consumerRecord.value(),
consumerRecord.topic(),
consumerRecord.offset())
)
.map(ConsumerRecord::value)
.subscribe(
m -> log.info("modtaget besked {}", m),
e -> log.error("fejl ved modtagelse af besked", e));
}

offentlig Flux getPeers() {
returnere peers.findAll();
}

offentlig Mono connectToPeer(Node node) {
returnere peers.save(node);
}
}

Enhver besked modtaget fra Kafka vil blot blive logget, og vi vil faktisk ikke gøre noget med noderne, bortset fra at lagre og liste dem.

Til sidst skal vi fortælle Spring Boot, hvor den kan finde delte komponenter og lagre. Vi kan kommentere hovedklassen:

@Slf4j
@SpringBootApplication
@ComponentScan(basePackageClasses = {
net.kamradtfamily.blockchain.api.Blockchain.class,
net.kamradtfamily.blockchainnode.Application.class})
@EnableReactiveMongoRepositories(basePackageClasses = {
net.kamradtfamily.blockchain.api.BlockRepository.class,
net.kamradtfamily.blockchainnode.NodeRepository.class})
public class Application {
offentlig statisk ugyldig hoved (String [] args) {
SpringApplication.run (Application.class, args);
prøve {
Egenskaber gitProps = new Properties();
gitProps.load(
Anvendelse
.klasse
.getResourceAsStream("/git.properties"));
log.info("Git-egenskaber:");
gitProps.entrySet().stream()
.forEach(es ->
log.info("{}: {}",
es.getKey(),
es.getValue()));
} fangst (undtagelse e) {
log.error("Fejl ved læsning af Git-egenskaber");
}
}
}

Foråret skal fortælles, hvor der skal scannes efter komponenter og lagre. Det ser normalt ud i pakken, som hovedklassen er i, men i vores tilfælde ønskede vi at dele komponenter fra net.kamradtfamily.blockchain.api. Så jeg tilføjede ComponentScan , EnableReactiveMongoRepositories anmærkninger. Jeg tilføjede også noget logning, så når det starter op, ved vi hvilken Git commit hash vi kører.

For at køre alt dette, er vi nødt til at flytte nogle havne rundt. For at have den nye service og den eksisterende service bliver vi nødt til at give hver af dem unikke eksterne porte. Lad os tilføje det til vores docker-compose-node1.yaml:

blockchainnode:
billede: rlkamradt/blockchainnode:1.0-SNAPSHOT
porte:
8080
miljø:
MONGO_HOST: mongo
SPRING_KAFKA_BOOTSTRAP-SERVERE: kafka:29092

MongoExpress-tjenesten optager allerede port 8081, så vi eksponerer den som 8082. Byg nu de nye billeder, træk dem, og kør dem alle:

mvn clean install jib:build
docker compose -f docker-compose-node1.yaml pull
docker compose -f docker-compose-node1.yaml op

Når du derefter opretter en transaktion med blockchain-tjenesten, vil du se i logfilerne for blockchainnode tjeneste, at en besked blev modtaget. Du vil også være i stand til at ramme slutpunkterne http://localhost:8082/node/peers og oprette og liste peers.

Det er her, tingene bliver komplicerede. Vi har brug for mere end én node, der kører, og vi har brug for, at noderne reagerer på meddelelser efter transaktioner eller blokeringer. Vi har også brug for, at noderne taler med hinanden under opstart eller tilføjelse af noder. Jeg vil kopiere SimpleNodeService , SimpleNodeController til NodeService , NodeController. Jeg vil efterlade de gamle klasser, hvis du ser på koden på GitHub og vil følge med, men jeg vil kommentere Component , RestController annoteringer, så de ikke kommer i gang under kørsel.

Jeg vil tilføje et ekstra slutpunkt til NodeController for at tillade bekræftelse af, at en transaktion har gjort den til en blok i alle noderne:

@GetMapping(sti = "transaktioner/:transaktionsId/bekræftelser",
producerer = MediaType.ALL_VALUE)
Mono getTransactionFromNode(
@RequestParam("transactionId") String transactionId) {
returnere nodeService
.getConfirmations(Lang.Værdi af(Transaktions ID))
.map(b -> b.toString());
}

Det betyder, at jeg har brug for et nyt sæt metoder i NodeService for at få bekræftelserne fra alle noderne:

offentlig Mono getConfirmation(Node peer, long transactionId) {
String URL = peer.getUrl()
+ "/blok/blokerer/transaktioner/"
+ transaktions-id;
log.info("Henter transaktioner fra: {}", URL);
tilbagevendende klient
.få()
.uri(URL)
.retrieve().bodyToMono(Blok.klasse);
.onErrorContinue((t, o) -> Mono.tom());
}

Mono getConfirmations(long transactionId) {
// Få optælling af peers med bekræftelser på, at transaktionen eksisterer
returnere blockchain
.findTransactionInChain(transactionId, blockchain
.getAllBlocks())
.zipWith(peers.findAll()
.flatMap(peer -> getConfirmation(peer,
Transaktions ID)))
.tælle();
}

Dette vil returnere en optælling af de noder, der har denne transaktion i en blok. Men først skal jeg oprette et nyt slutpunkt i BlockController, en der vil rapportere, hvis transaktionen er i en blok i blockchain.

@GetMapping(sti = "blocks/transaction/{transactionId}", 
producerer = MediaType.APPLICATION_JSON_VALUE)
offentlig Mono getTransaction(
@PathVariable("transactionId") Lang transaktions-id) {
returnere blockchain
.findTransactionInChain(transactionId,
blockchain.getAllBlocks())
.last() // antag, at der kun er én
.switchIfEmpty(Mono.fejl(ny ResponseStatusException(
Http Status.IKKE FUNDET,
"Transaktion ikke fundet i Blockchain")));

Heldigvis har vi allerede en metode findTransactionInChain som vil returnere den blok, som en transaktion er fundet i.

Dernæst skal vi svare på beskeder fra Kafka. Vi tilføjer en messageHandler metode, der vil udsende beskeder til alle peer noder:

offentlig Mono messageHandler(Besked m) {
if("addedBlock".equals(m.getMessage())) {
returner peers.findAll()
.flatMap(p -> sendLatestBlock(p, m.getBlock()))
.switchIfEmpty(Mono.just(m.getBlock()))
.sidst();
} else if("addedTransaction".equals(m.getMessage())) {
returner peers.findAll()
.flatMap(p -> sendTransaction(p, m.getTransaction()))
.switchIfEmpty(Mono.just(m.getTransaction()))
.sidst();
} else if("getBlocks".equals(m.getMessage())) {
returner peers.findAll()
.flatMap(p -> getBlocks(p))
.switchIfEmpty(blockchain.getLastBlock())
.sidst();
} Else {
log.error("ukendt meddelelse {}", m);
returner Mono.empty();
}
}

Dette kræver to nye metoder til at foretage anmodninger til andre noder:

offentlig Mono sendLatestBlock(Node peer, 
Bloker blok) {
String URL = peer.getUrl() + "/block/blocks/latest";
log.info("Sender seneste blok til: {}", URL);
tilbagevendende klient
.sætte()
.uri(URL)
.body(blok, blok.klasse)
.udveksle();
}

offentlig Mono sendTransaction(Node peer,
Transaktionstransaktion) {
String URL = peer.getUrl() + "/transaction";
log.info("Sender transaktion '{}' til: {}", transaktion, URL);
tilbagevendende klient
.stolpe()
.uri(URL)
.body(transaktion, Transaktion.klasse)
.udveksle();
}

Vi har allerede POST til /transaction slutpunkt, men vi skal tilføje PUT til /block/blocks/latest slutpunkt.

@PutMapping(sti = "/blok/blokke/nyeste", 
producerer = MediaType.APPLICATION_JSON_VALUE)
offentlig Mono checkReceivedBlock(
@RequestBody Bloker modtagetBlock) {
returner blokkæde.checkReceivedBlock(receivedBlock);
}

Dette kræver en ny metode i Blockchain service.

offentlig Mono checkReceivedBlock(Bloker modtagetBlok) {
returner getLastBlock()
.filter(b -> b.getIndex() < modtagetBlock.getIndex())
.flatMap(b -> {
log.info(
"Blockchain muligvis bagud. Vi fik: {}, Peer fik: {}",
b.getIndex(),
modtagetBlock.getIndex());
if (b.getHash().equals(receivedBlock.getPreviousHash())) {
log.info("Tilføjer modtaget blok til vores kæde");
returner addBlock(receivedBlock, true);
} Else {
log.info("Forespørgselskæde fra vores jævnaldrende");
emitter.send(TOPIC, Message.builder()
.type("tom")
.message("getBlocks")
.build())
.subscribe();
returner Mono.empty();
}
});
}

Du kan se, hvordan nodelaget taler med andre noder via den offentlige API. Der er dog et problem. Hver node er repræsenteret af en enkelt URL, men vi skal tale med to separate tjenester: bloktjenesten og nodetjenesten. Jeg vil lave en simpel indgang med en Nginx-instans. På den måde kan vi tale med begge tjenester (og flere senere) med en enkelt URL. Du kan se på koden på GitHub for detaljer om Nginx-konfigurationen og tilføjelse af alle tjenester til docker-compose-node1.yaml.

Ved at starte det hele fungerer alle endepunkter stadig, og jeg kan se i logfilerne kommunikationen mellem blockchain-tjenesten og node-tjenesten på Kafka-bussen. Nu er det tid til at lave en anden node. Kopier docker-compose-node1.yaml til docker-compose-node2.yaml og skifte den eksterne port på Nginx-tjenesten fra 8080 til 8081 for at nå node 1 ved port 8080 og node 2 ved port 8081. Jeg vil også lave et lille script kaldet startnode1 for at starte hver tjeneste i rækkefølge og udlæse logfilerne fra nodetjenesten:

docker compose -p node1 -f docker-compose-node1.yaml up -d mongo
docker compose -p node1 -f docker-compose-node1.yaml up -d zookeeper
docker compose -p node1 -f docker-compose-node1.yaml up -d mongo-express
docker compose -p node1 -f docker-compose-node1.yaml up -d kafka
docker compose -p node1 -f docker-compose-node1.yaml up -d blockchain
docker compose -p node1 -f docker-compose-node1.yaml up blockchainnode

Fordi den sidste linje ikke har en -d flag, viser det logfilerne, indtil Ctrl-C stopper det. Jeg bruger -p node1 flag, så jeg kan oprette separate forekomster af tjenesterne. Kopier derefter til en fil kaldet startnode2, men udskift Docker compose-filen for at starte node 2 og -p flag til node2. Glem ikke at indstille det eksekverbare flag på hver:

chmod +x startnode1
chmod +x startnode2

Der er en sidste ændring. Jeg-medlemmet af node-tjenesten skal have URL'en som set af andre tjenester, så brug af lokal vært duer ikke. Jeg har oprettet en Spring-ejendom i application.properties:

server.myself: http://localhost:8080

Så tilsidesætter jeg det i docker-compose-node1.yaml, som nu ser sådan ud:

version: '3.1'
tjenester:
mongo:
billede: mongo
genstart: altid
porte:
- 27017
miljø:
MONGO_INITDB_ROOT_USERNAME: rod
MONGO_INITDB_ROOT_PASSWORD: eksempel
dyrepasser:
billede: confluentinc/cp-zookeeper:nyeste
porte:
- 2181
miljø:
ZOOKEEPER_CLIENT_PORT: 2181
kafka:
billede: confluentinc/cp-kafka:nyeste
porte:
- 9092
- 29092
venstre:
- dyrepasser
miljø:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: dyrepasser:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
blockchain:
billede: rlkamradt/blockchain:1.0-SNAPSHOT
porte:
- 8080
miljø:
SERVER_MIG SELV: http://192.168.0.174:8080
MONGO_HOST: mongo
SPRING_KAFKA_BOOTSTRAP-SERVERE: kafka:29092
blockchainnode:
billede: rlkamradt/blockchainnode:1.0-SNAPSHOT
porte:
- 8080
miljø:
SERVER_MIG SELV: http://192.168.0.174:8080
MONGO_HOST: mongo
SPRING_KAFKA_BOOTSTRAP-SERVERE: kafka:29092
nginx:
billede: nginx: nyeste
mængder:
- ./nginx.conf:/etc/nginx/nginx.conf
porte:
8080

docker-compose-node2.yaml ændrer også porten til 8081 for både SERVER_MYSELF og nginx.ports værdi.

Start begge instanser. Når de begge kører, kan du forbinde den ene til den anden:

Nu kan du oprette transaktioner og mineblokke, som vist i forrige artikel, men du kan liste transaktionerne og blokkene i begge noder. Peer-to-peer-protokollen sikrer, at begge noder har de samme data.

Jeg vil ikke sige, at det er perfekt på nuværende tidspunkt. Der er mange forskellige sekvenser, der skal testes for at sikre, at uanset hvordan tingene gøres, forbliver blockchainen i de forskellige instanser den samme. Men denne artikel er allerede lang nok, og jeg er sikker på, at du ikke vil læse om, at jeg fejlretter dette web!

Tak fordi du læste denne ret lange artikel, jeg har forsøgt at kondensere den for at gøre den så kortfattet som muligt, men det er et meget kompliceret emne.

Jeg tror, ​​at den næste artikel i denne serie bliver lidt enklere. Vi vil diskutere den sidste del af puslespillet: minearbejdere og brugere.

Al koden til denne artikel kan findes her:

Den forrige artikel i denne serie:

Kilde: https://betterprogramming.pub/build-a-set-of-blockchains-using-java-d99cd866931b?source=rss——-8—————–cryptocurrency

Tidsstempel:

Mere fra Medium