Créez un ensemble de blockchains à l'aide de Java PlatoBlockchain Data Intelligence. Recherche verticale. Aï.

Construire un ensemble de blockchains à l'aide de Java

Confiance via un réseau peer-to-peer

toile d'araignée
Image ClaudiaWollesen De Pixabay

Dans la première partie de cette série, nous avons créé une seule blockchain. Nous allons maintenant en créer un ensemble et les faire parler entre eux. Le véritable intérêt de la blockchain est un système de vérification distribué. Vous pouvez ajouter des blocs à partir de n'importe quel nœud et éventuellement atteindre des nœuds homologues afin que tout le monde soit d'accord sur ce à quoi ressemble la blockchain. Ceci est important car vous ne voulez pas d’une source unique de vérité dans un système distribué.

Il y a un problème qui surgit immédiatement : chaque nœud est constitué de deux services, plus un bus de messages MongoDB et Kafka qui doivent tous communiquer entre eux. Mais je souhaite tester et démontrer plusieurs nœuds sur un seul hôte (mon ordinateur portable). J'utilise Docker Compose, je vais donc devoir créer un fichier Docker Compose pour chaque nœud afin d'aider à garder les ports droits.

Nous travaillerons sur un service de nœuds qui permettra aux nœuds de fonctionner les uns avec les autres. Cela obtiendra des entrées de deux endroits, une interface reposante qui vous permet d'ajouter et de lister les nœuds connectés, et un bus de messages fourni par Kafka qui informe le service de nœud des modifications dans la blockchain locale qui doivent être diffusées aux nœuds homologues.

Pour que les services puissent être utilisés comme images, j'utiliserai le plugin Google Jib maven. C'est le moyen le plus simple de créer une image à partir d'une version Maven. Nous ajoutons ce qui suit au fichier pom de chaque module pour lequel nous avons besoin d'une image :




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


Dans notre cas, nous pouvons utiliser les valeurs par défaut pour la configuration. Ensuite, vous pouvez exécuter mvn clean install jib:build et cela créera une image que vous pourrez utiliser dans votre fichier de composition Docker.

Prenons ce que nous avons jusqu'à présent et commençons tout avec un fichier de composition Docker tout compris appelé docker-compose-node1:

version : '3.1'
services:
mongo :
image : mongo
redémarrer : toujours
ports :
à 27017
environnement:
MONGO_INITDB_ROOT_USERNAME : racine
MONGO_INITDB_ROOT_PASSWORD : exemple
mongo-express :
image : mongo-express
redémarrer : toujours
ports :
- 8081: 8081
environnement:
ME_CONFIG_MONGODB_ADMINUSERNAME : racine
ME_CONFIG_MONGODB_ADMINPASSWORD : exemple
gardien de zoo :
image : confluentinc/cp-zookeeper:dernière
ports :
à 2181
environnement:
ZOOKEEPER_CLIENT_PORT : 2181
kafka :
image : confluentinc/cp-kafka : dernière
ports :
à 9092
à 29092
gauche:
- gardien de zoo
environnement:
KAFKA_BROKER_ID : 1
KAFKA_ZOOKEEPER_CONNECT : gardien de zoo : 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 :
image : rlkamradt/blockchain:1.0-INSTANTANÉ
ports :
- 8080: 8080
environnement:
MONGO_HOST : mongo
SPRING_KAFKA_BOOTSTRAP-SERVERS : kafka : 29092

Vous remarquerez que certaines choses ont changé. Tout d’abord, j’ai supprimé le port externe de Kafka afin qu’il ne soit accessible qu’à l’intérieur du réseau de composition. Ensuite, j'ai ajouté l'application blockchain avec l'image créée par la construction. Enfin, j'ai remplacé quelques propriétés Spring avec la variable d'environnement afin qu'elle accède à Mongo et Kafka depuis l'intérieur du réseau Compose. Compose créera des entrées DNS afin que les services exécutés à partir du même fichier de composition puissent accéder les uns aux autres. Exécutez-le avec cette commande docker compose -f docker-compose-node1.yaml up -d et assurez-vous que vous pouvez toujours accéder à l'API blockchain de base :

Notre application est maintenant opérationnelle et a créé le bloc Genesis.

Il existe déjà du code dans le service blockchain qui envoie un message à Kafka chaque fois que nous ajoutons un bloc ou ajoutons une transaction. Nous devons créer un nouveau service qui lira ces événements et les diffusera à une liste de pairs. Supprimons ce que nous avons en cours d'exécution pour le moment et ajoutons un simple service de nœud qui enregistrera un message lorsqu'il sera reçu. Nous aurons besoin d'un nouveau module dans le projet - ce sera un autre service Spring Boot et il pourra communiquer avec des nœuds externes.

Tout d’abord, nous devrons définir un nœud, qui n’est qu’une URL. Ici se trouve le Node:

@Données
@Constructeur
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@Document
Nœud de classe publique {
@Identifiant
identifiant de chaîne privé ;
URL de chaîne privée ;
}

Nous le stockons dans une collection dans MongoDB, nous avons donc besoin d'une interface de référentiel :

@Dépôt
l'interface publique NodeRepository s'étend
ReactiveMongoRepository {
Mono findByUrl(String url);
}

Ensuite, nous avons besoin d'un contrôleur pour pouvoir voir et ajouter Node objets:

@Slf4j
@RestController
@RequestMapping("/node")
classe publique SimpleNodeController {
blockchain Blockchain finale privée ;
privé final SimpleNodeService simpleNodeService;

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

@GetMapping(path = "pairs", produit =
Type de support.TEXT_EVENT_STREAM_VALUE)
Flux getNodes() {
return simpleNodeService.getPeers();
}

@PostMapping(path = "pairs", produit =
Type de support.APPLICATION_JSON_VALUE)
Mono addNode (@RequestBody Node node) {
return simpleNodeService.connectToPeer(node);
}

}

Ensuite, nous avons besoin d’une classe de service simple qui interagira avec MongoDB et Kafka :

@Composant
@Slf4j
classe publique SimpleNodeService {
Nœud statique privé moi-même;
hôte de chaîne privé final ;
port international privé final ;
homologues privés finaux de NodeRepository ;
blockchain Blockchain privée finale ;
final privé ReactiveKafkaConsumerTemplate<String,
Message> émetteur ;

public SimpleNodeService (@Value("${server.host}") Hôte de chaîne,
@Value("${server.port}") Port de chaîne,
Blockchain blockchain,
Homologues de NodeRepository,
ReactiveKafkaConsumerTemplate émetteur) {
this.host = hôte ;
this.port = Entier.analyserInt(port);
this.blockchain = blockchain ;
this.emitter = émetteur;
this.peers = pairs ;
moi-même = Nœud.constructeur()
.url("http://" + hôte + ":" + port)
.construire();
émetteur
.receiveAutoAck()
.doOnNext(consumerRecord -> enregistrer.Info(
"clé reçue={}, valeur={} du sujet={}, offset={}",
consumerRecord.key(),
consumerRecord.value(),
consumerRecord.topic(),
consumerRecord.offset())
)
.map (ConsumerRecord :: valeur)
.s'abonner(
m -> enregistrer.info("message reçu {}", m),
e -> enregistrer.error("erreur de réception du message", e));
}

public Flux getPeers() {
return pairs.findAll();
}

public Mono connectToPeer(Node node) {
return peers.save(node);
}
}

Tout message reçu de Kafka sera simplement enregistré et nous ne ferons rien avec les nœuds, à part les stocker et les lister.

Enfin, nous devons indiquer à Spring Boot où il peut trouver les composants et les référentiels partagés. Nous pouvons annoter la classe principale :

@Slf4j
@SpringBootApplication
@ComponentScan(basePackageClasses = {
net.kamradtfamily.blockchain.api.Blockchain.class,
net.kamradtfamily.blockchainnode.Application.class})
@EnableReactiveMongoRdépôts (basePackageClasses = {
net.kamradtfamily.blockchain.api.BlockRepository.class,
net.kamradtfamily.blockchainnode.NodeRepository.class})
Application de classe publique {
public static void main (String [] arguments) {
SpringApplication.run(Application.class, args);
essayez {
Propriétés gitProps = new Properties();
gitProps.load(
Application
.classe
.getResourceAsStream("/git.properties"));
log.info("Propriétés Git :");
gitProps.entrySet().stream()
.forEach(es ->
log.info("{} : {}",
es.getKey(),
es.getValue()));
} catch (Exception e) {
log.error("Erreur lors de la lecture des propriétés Git");
}
}
}

Spring doit savoir où rechercher les composants et les référentiels. Cela apparaît généralement dans le package dans lequel se trouve la classe principale, mais dans notre cas, nous voulions partager des composants de net.kamradtfamily.blockchain.api. J'ai donc ajouté le ComponentScan et de EnableReactiveMongoRepositories annotations. J'ai également ajouté de la journalisation afin qu'à chaque démarrage, nous sachions quel hachage de validation Git nous utilisons.

Afin d’exécuter tout cela, nous devons déplacer certains ports. Afin d'avoir le nouveau service et le service existant, nous devrons donner à chacun d'eux des ports externes uniques. Ajoutons cela à notre docker-compose-node1.yaml:

nœud blockchain :
image : rlkamradt/blockchainnode:1.0-INSTANTANÉ
ports :
- 8080: 8082
environnement:
MONGO_HOST : mongo
SPRING_KAFKA_BOOTSTRAP-SERVERS : kafka : 29092

Le service MongoExpress utilise déjà le port 8081, nous allons donc l'exposer sous le nom 8082. Créez maintenant les nouvelles images, extrayez-les et exécutez-les toutes :

mvn clean install jib: construire
docker compose -f docker-compose-node1.yaml pull
docker compose -f docker-compose-node1.yaml vers le haut

Ensuite, lorsque vous créez une transaction avec le service blockchain, vous verrez dans les journaux du blockchainnode service qu'un message a été reçu. Vous pourrez également atteindre les points finaux http://localhost:8082/node/peers et créer et lister des pairs.

C’est ici que les choses se compliquent. Nous avons besoin de plus d'un nœud en cours d'exécution et nous avons besoin que les nœuds répondent aux messages après l'ajout d'une transaction ou d'un bloc. Nous avons également besoin que les nœuds communiquent entre eux lors du démarrage ou de l'ajout de nœuds. je vais copier SimpleNodeService et de SimpleNodeController à NodeService et de NodeController. Je vais laisser les anciennes classes au cas où vous regarderiez le code sur GitHub et voudriez suivre, mais je vais commenter le Component et de RestController annotations afin qu’elles ne démarrent pas au moment de l’exécution.

Je vais ajouter un point de terminaison supplémentaire au NodeController pour permettre de confirmer qu'une transaction a été transformée en bloc dans tous les nœuds :

@GetMapping(path = "transactions/:transactionId/confirmations",
produit = MediaType.ALL_VALUE)
Mono getTransactionFromNode(
@RequestParam("transactionId") Chaîne transactionId) {
retourner le nœudService
.getConfirmations(Long.valeur de(identifiant de transaction))
.map(b -> b.toString());
}

Cela signifie que j'ai besoin d'un nouvel ensemble de méthodes dans le NodeService pour obtenir les confirmations de tous les nœuds :

public Mono getConfirmation (homologue de nœud, long transactionId) {
URL de chaîne = peer.getUrl()
+ "/bloc/blocs/transactions/"
+ ID de transaction ;
enregistrer.info("Obtenir des transactions à partir de : {}", URL);
retourner le client
.obtenir()
.uri(URL)
.retrieve().bodyToMono(Block.class);
.onErrorContinue((t, o) -> Mono.vide());
}

Mono getConfirmations(long transactionId) {
// Récupère le nombre de pairs avec confirmation que la transaction existe
retourner la blockchain
.findTransactionInChain (ID de transaction, blockchain
.getAllBlocks())
.zipWith(peers.findAll()
.flatMap (homologue -> getConfirmation (homologue,
identifiant de transaction)))
.compter();
}

Cela renverra le nombre de nœuds qui ont cette transaction dans un bloc. Mais d'abord, je dois créer un nouveau point de terminaison dans le BlockController, celui qui indiquera si la transaction se trouve dans un bloc de la blockchain.

@GetMapping(chemin = "blocs/transaction/{transactionId}", 
produit = MediaType.APPLICATION_JSON_VALUE)
public Mono getTransaction(
@PathVariable("transactionId") Long transactionId) {
retourner la blockchain
.findTransactionInChain(ID de transaction,
blockchain.getAllBlocks())
.last() // suppose qu'il n'y en a qu'un
.switchIfEmpty(Mono.erreur(nouvelle ResponseStatusException(
Statut HTTP.PAS TROUVÉ,
"Transaction introuvable dans la blockchain")));

Heureusement, nous avons déjà une méthode findTransactionInChain cela renverra le bloc dans lequel se trouve une transaction.

Ensuite, nous devons répondre aux messages de Kafka. Nous ajouterons un messageHandler méthode qui diffusera des messages à tous les nœuds homologues :

publicMono messageHandler (Message m) {
if("addedBlock".equals(m.getMessage())) {
retourner pairs.findAll()
.flatMap(p -> sendLatestBlock(p, m.getBlock()))
.switchIfEmpty(Mono.just(m.getBlock()))
.dernier();
} else if("addedTransaction".equals(m.getMessage())) {
retourner pairs.findAll()
.flatMap(p -> sendTransaction(p, m.getTransaction()))
.switchIfEmpty(Mono.just(m.getTransaction()))
.dernier();
} else if("getBlocks".equals(m.getMessage())) {
retourner pairs.findAll()
.flatMap(p -> getBlocks(p))
.switchIfEmpty(blockchain.getLastBlock())
.dernier();
} Else {
log.error("message inconnu {}", m);
return Mono.empty();
}
}

Cela nécessite deux nouvelles méthodes pour effectuer des requêtes vers d'autres nœuds :

public Mono sendLatestBlock (homologue de nœud, 
Bloc bloc) {
Chaîne URL = peer.getUrl() + "/block/blocks/latest" ;
enregistrer.info("Publication du dernier bloc sur : {}", URL);
retourner le client
.mettre()
.uri(URL)
.body(bloc, Bloc.class)
.échange();
}

public Mono sendTransaction (homologue de nœud,
Transaction transaction) {
URL de chaîne = peer.getUrl() + "/transaction" ;
enregistrer.info("Envoi de la transaction '{}' à : {}", transaction, URL);
retourner le client
.poste()
.uri(URL)
.body(transaction, Transaction.class)
.échange();
}

Nous avons déjà le POST vers le /transaction point final, mais nous devons ajouter le PUT au /block/blocks/latest point final.

@PutMapping(chemin = "/block/blocks/latest", 
produit = MediaType.APPLICATION_JSON_VALUE)
public Mono checkReceivedBlock(
@RequestBody Block reçuBlock) {
return blockchain.checkReceivedBlock(receivedBlock);
}

Cela nécessite une nouvelle méthode dans le Blockchain après-vente.

public Mono checkReceivedBlock(Bloc reçuBlock) {
retourner getLastBlock()
.filter(b -> b.getIndex() <reçuBlock.getIndex())
.flatMap(b -> {
log.info(
"La blockchain est peut-être derrière. Nous avons : {}, Peer a : {}",
b.getIndex(),
reçuBlock.getIndex());
if (b.getHash().equals(receivedBlock.getPreviousHash())) {
log.info("Ajout du bloc reçu à notre chaîne");
return addBlock (receivedBlock, true);
} Else {
log.info("Chaîne d'interrogation de nos pairs");
émetteur.envoyer(SUJET, Message.builder()
.type("vide")
.message("getBlocks")
.construire())
.s'abonner();
return Mono.empty();
}
});
}

Vous pouvez voir comment la couche de nœuds communique avec les autres nœuds via l'API publique. Mais il y a un problème. Chaque nœud est représenté par une seule URL, mais nous devons parler à deux services distincts : le service de bloc et le service de nœud. Je vais faire une simple entrée avec une instance Nginx. De cette façon, nous pouvons communiquer avec les deux services (et plus tard) avec une seule URL. Vous pouvez consulter le code sur GitHub pour plus de détails sur la configuration de Nginx et l'ajout de tous les services au docker-compose-node1.yaml.

Au démarrage, tous les points de terminaison fonctionnent toujours et je peux voir dans les journaux la communication entre le service blockchain et le service nœud sur le bus Kafka. Il est maintenant temps de créer un deuxième nœud. Copiez le docker-compose-node1.yaml à docker-compose-node2.yaml et basculez le port externe du service Nginx de 8080 à 8081 afin d'atteindre le nœud 1 au port 8080 et le nœud 2 au port 8081. Je vais également créer un petit script appelé startnode1 pour démarrer chaque service dans l'ordre et afficher les journaux du service de nœud :

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 jusqu'à blockchainnode

Parce que la dernière ligne n'a pas de -d drapeau, il affiche les journaux jusqu'à ce que Ctrl-C l'arrête. Je utilise l -p node1 flag afin que je puisse créer des instances distinctes des services. Ensuite, copiez dans un fichier appelé startnode2, mais remplacez le fichier de composition Docker pour démarrer le nœud 2 et le -p indicateur vers node2. N'oubliez pas de définir le drapeau exécutable sur chacun :

chmod +x noeud de démarrage1
chmod +x noeud de démarrage2

Il y a un dernier changement. Le membre moi-même du service de nœud doit avoir l'URL telle que vue par d'autres services, donc l'utilisation de l'hôte local ne suffira pas. J'ai configuré une propriété Spring dans le fichier application.properties :

serveur.moi-même : http://localhost:8080

Ensuite, je le remplace dans le docker-compose-node1.yaml qui ressemble maintenant à ceci :

version : '3.1'
services:
mongo :
image : mongo
redémarrer : toujours
ports :
à 27017
environnement:
MONGO_INITDB_ROOT_USERNAME : racine
MONGO_INITDB_ROOT_PASSWORD : exemple
gardien de zoo :
image : confluentinc/cp-zookeeper:dernière
ports :
à 2181
environnement:
ZOOKEEPER_CLIENT_PORT : 2181
kafka :
image : confluentinc/cp-kafka : dernière
ports :
à 9092
à 29092
gauche:
- gardien de zoo
environnement:
KAFKA_BROKER_ID : 1
KAFKA_ZOOKEEPER_CONNECT : gardien de zoo : 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 :
image : rlkamradt/blockchain:1.0-INSTANTANÉ
ports :
à 8080
environnement:
SERVEUR_MYSELF : http://192.168.0.174:8080
MONGO_HOST : mongo
SPRING_KAFKA_BOOTSTRAP-SERVERS : kafka : 29092
nœud blockchain :
image : rlkamradt/blockchainnode:1.0-INSTANTANÉ
ports :
à 8080
environnement:
SERVEUR_MYSELF : http://192.168.0.174:8080
MONGO_HOST : mongo
SPRING_KAFKA_BOOTSTRAP-SERVERS : kafka : 29092
nginx :
image : nginx : dernière
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
ports :
- 8080: 80

La docker-compose-node2.yaml change également le port en 8081 pour le SERVER_MYSELF et les terres parsemées de nginx.ports valeur.

Démarrez les deux instances. Lorsqu’ils fonctionnent tous les deux, vous pouvez les connecter l’un à l’autre :

Vous pouvez désormais créer des transactions et des blocs miniers, comme démontré dans le article précédent, mais vous pouvez lister les transactions et les blocs dans les deux nœuds. Le protocole peer-to-peer garantit que les deux nœuds disposent des mêmes données.

Je ne dirai pas que c’est parfait à ce stade. De nombreuses séquences différentes doivent être testées pour garantir que, quelle que soit la manière dont les choses sont faites, la blockchain dans les différentes instances reste la même. Mais cet article est déjà assez long et je suis sûr que vous ne voulez pas entendre parler de moi déboguant ce site !

Merci d’avoir lu cet article assez long, j’ai essayé de le condenser pour le rendre le plus concis possible mais c’est un sujet très compliqué.

Je pense que le prochain article de cette série sera un peu plus simple. Nous discuterons de la dernière partie du puzzle : les mineurs et les utilisateurs.

Tout le code de cet article peut être trouvé ici :

L'article précédent de cette série :

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

Horodatage:

Plus de Moyenne