Costruisci un set di blockchain utilizzando Java PlatoBlockchain Data Intelligence. Ricerca verticale. Ai.

Costruisci un set di blockchain usando Java

Fiducia tramite reti peer-to-peer

ragnatela
Immagine di Claudia Wollesen da Pixabay

Nella prima parte di questa serie, abbiamo realizzato un'unica blockchain. Ora ne faremo una serie e li faremo parlare tra loro. Il vero punto della blockchain è un sistema distribuito di verifica. Puoi aggiungere blocchi da qualsiasi nodo e alla fine arriva ai nodi peer in modo che tutti siano d'accordo sull'aspetto della blockchain. Questo è importante perché non vuoi una singola fonte di verità in un sistema distribuito.

C'è un problema che si presenta subito: ogni nodo è costituito da due servizi, più un MongoDB e un bus di messaggi Kafka che devono tutti dialogare tra loro. Ma voglio testare e dimostrare più nodi su un singolo host (il mio laptop). Sono stato in esecuzione con Docker compose, quindi dovrò creare un file di composizione Docker per ogni nodo per mantenere le porte diritte.

Lavoreremo su un servizio di nodo che consentirà ai nodi di lavorare tra loro. Questo riceverà input da due posti, un'interfaccia riposante che ti consente di aggiungere ed elencare i nodi collegati e un bus di messaggi fornito da Kafka che notifica al servizio del nodo le modifiche nella blockchain locale che devono essere trasmesse ai nodi peer.

Affinché i servizi possano essere utilizzati come immagini, utilizzerò il plug-in Maven di Google Jib. Questo è il modo più semplice per creare un'immagine da una build Maven. Aggiungiamo quanto segue al file pom di ogni modulo per il quale abbiamo bisogno di un'immagine:




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


Nel nostro caso, possiamo utilizzare i valori predefiniti per la configurazione. Successivamente, puoi correre mvn clean install jib:build e creerà un'immagine che puoi utilizzare nel tuo file di composizione Docker.

Prendiamo ciò che abbiamo finora e avviamo tutto con un file di composizione Docker all-inclusive chiamato docker-compose-node1:

versione: '3.1'
servizi:
mongo:
immagine: mongo
riavvia: sempre
porti:
- 27017
ambiente:
MONGO_INITDB_ROOT_USERNAME: radice
MONGO_INITDB_ROOT_PASSWORD: esempio
mongo-espresso:
immagine: mongo-express
riavvia: sempre
porti:
- 8081: 8081
ambiente:
ME_CONFIG_MONGODB_ADMINUSERNAME: radice
ME_CONFIG_MONGODB_ADMINPASSWORD: esempio
guardiano dello zoo:
immagine: confluentinc/cp-zookeeper:latest
porti:
- 2181
ambiente:
ZOOKEEPER_CLIENT_PORT: 2181
cafka:
immagine: confluentinc/cp-kafka:latest
porti:
- 9092
- 29092
sinistra:
- guardiano dello zoo
ambiente:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: guardiano dello zoo:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT: PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: TESTO PLAIN
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
blockchain:
immagine: rlkamradt/blockchain:1.0-SNAPSHOT
porti:
- 8080: 8080
ambiente:
MONGO_HOST: mongo
SPRING_KAFKA_BOOTSTRAP-SERVER: kafka:29092

Noterai che alcune cose sono cambiate. Innanzitutto, ho rimosso la porta esterna da Kafka in modo che sia accessibile solo all'interno della rete di composizione. Quindi ho aggiunto l'app blockchain con l'immagine creata dalla build. Infine, ho sovrascritto alcune proprietà di primavera con la variabile d'ambiente in modo che acceda a mongo e Kafka dall'interno della rete di composizione. Compose creerà voci DNS in modo che i servizi eseguiti dallo stesso file di composizione possano accedersi a vicenda. Eseguilo con questo comando docker compose -f docker-compose-node1.yaml up -d e assicurati di poter ancora raggiungere l'API blockchain di base:

Ora la nostra applicazione è attiva e funzionante e ha creato il blocco genesi.

C'è già un codice nel servizio blockchain che invia un messaggio a Kafka ogni volta che aggiungiamo un blocco o aggiungiamo una transazione. Dobbiamo creare un nuovo servizio che leggerà quegli eventi e li trasmetterà a un elenco di colleghi. Riduciamo ciò che abbiamo in esecuzione per ora e aggiungiamo un semplice servizio di nodo che registrerà un messaggio quando viene ricevuto. Avremo bisogno di un nuovo modulo nel progetto: questo sarà un altro servizio Spring Boot e sarà in grado di parlare con nodi esterni.

Innanzitutto, dovremo definire un nodo, che è solo un URL. Ecco il Node:

@Dati
@Costruttore
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties (ignoreUnknown = true)
@Documento
nodo di classe pubblica {
@id
ID stringa privato;
URL stringa privato;
}

Lo stiamo archiviando in una raccolta nel MongoDB, quindi abbiamo bisogno di un'interfaccia repository:

@Repository
l'interfaccia pubblica NodeRepository si estende
ReactiveMongoRepository {
Mono findByUrl(URL stringa);
}

Successivamente, abbiamo bisogno di un controller per poter vedere e aggiungere Node oggetti:

@ Slf4j
@RestController
@RequestMapping("/node")
classe pubblica SimpleNodeController {
blockchain finale Blockchain privata;
SimpleNodeService finale privato simpleNodeService;

SimpleNodeController pubblico (blockchain blockchain,
SimpleNodeServicesempliceNodeService) {
this.simpleNodeService = simpleNodeService;
this.blockchain = blockchain;
}

@GetMapping(percorso = "coetanei", produce =
Tipo di supporto.TEXT_EVENT_STREAM_VALUE)
Flusso getNodes() {
restituisce simpleNodeService.getPeers();
}

@PostMapping(percorso = "coetanei", produce =
Tipo di supporto.APPLICATION_JSON_VALUE)
Mono addNode(@NodoRequestBody Node) {
restituisce simpleNodeService.connectToPeer(node);
}

}

Quindi, abbiamo bisogno di una semplice classe di servizio che interagisca con MongoDB e Kafka:

@Componente
@ Slf4j
classe pubblica SimpleNodeService {
nodo statico privato me;
host stringa privato finale;
porto privato finale;
peer NodeRepository privati ​​finali;
blockchain privata Blockchain finale;
finale privato ReactiveKafkaConsumerTemplate
Messaggio> emettitore;

public SimpleNodeService(@Value("${server.host}") String host,
@Value("${server.port}") Porta stringa,
Blockchain Blockchain,
peer di NodeRepository,
ReactiveKafkaConsumerTemplate emettitore) {
this.host = ospite;
this.port = Intero.analizzareInt(porta);
this.blockchain = blockchain;
this.emitter = emettitore;
this.peers = pari;
me = Nodo.costruttore()
.url("http://" + host + ":" + porta)
.costruire();
emettitore
.receiveAutoAck()
.doOnNext(consumerRecord -> ceppo.Informazioni(
"chiave ricevuta={}, valore={} dall'argomento={}, offset={}",
consumatoreRecord.key(),
consumatoreRecord.value(),
consumatoreRecord.topic(),
consumatoreRecord.offset())
)
.map(ConsumerRecord::valore)
.sottoscrivi(
m -> ceppo.info("messaggio ricevuto {}", m),
e -> ceppo.error("Errore durante la ricezione del messaggio", e));
}

flusso pubblico getPeers() {
restituisce peers.findAll();
}

Mono pubblico connectToPeer(nodo nodo) {
return peers.save(nodo);
}
}

Qualsiasi messaggio ricevuto da Kafka verrà semplicemente registrato e in realtà non faremo nulla con i nodi, a parte archiviarli ed elencarli.

Infine, dobbiamo dire a Spring Boot dove può trovare componenti e repository condivisi. Possiamo annotare la classe principale:

@ Slf4j
@SpringBootApplicazionen
@ComponentScan(basePackageClasses = {
net.kamradtfamily.blockchain.api.Blockchain.class,
net.kamradtfamily.blockchainnode.Application.class})
@EnableReactiveMongoRarchivi(basePackageClasses = {
net.kamradtfamily.blockchain.api.BlockRepository.class,
net.kamradtfamily.blockchainnode.NodeRepository.class})
applicazione di classe pubblica {
public static void main (String [] args) {
SpringApplication.run (Application.class, args);
provare {
Proprietà gitProps = new Properties();
gitProps.load(
Applicazioni
.classe
.getResourceAsStream("/git.properties"));
log.info("Proprietà Git:");
gitProps.entrySet().stream()
.forEach(es ->
log.info("{}: {}",
es.getKey(),
es.getValue()));
} catch (Exception e) {
log.error("Errore durante la lettura delle proprietà Git");
}
}
}

È necessario dire a Spring dove cercare componenti e repository. Di solito sembra nel pacchetto in cui si trova la classe principale, ma nel nostro caso volevamo condividere i componenti da net.kamradtfamily.blockchain.api. Quindi ho aggiunto il ComponentScan ed EnableReactiveMongoRepositories annotazioni. Ho anche aggiunto un po' di registrazione così ogni volta che si avvia sapremo quale hash di commit Git stiamo eseguendo.

Per eseguire tutto questo, dobbiamo spostare alcune porte. Per avere il nuovo servizio e il servizio esistente, dovremo assegnare a ciascuno di essi porte esterne univoche. Aggiungiamolo al ns docker-compose-node1.yaml:

nodo blockchain:
immagine: rlkamradt/blockchainnode:1.0-SNAPSHOT
porti:
- 8080: 8082
ambiente:
MONGO_HOST: mongo
SPRING_KAFKA_BOOTSTRAP-SERVER: kafka:29092

Il servizio MongoExpress sta già occupando la porta 8081, quindi la esporremo come 8082. Ora costruisci le nuove immagini, estraili ed eseguile tutte:

mvn installazione pulita fiocco:build
docker compose -f docker-compose-node1.yaml pull
docker compose -f docker-compose-node1.yaml su

Quindi quando crei una transazione con il servizio blockchain, vedrai nei log di blockchainnode servizio che è stato ricevuto un messaggio. Sarai anche in grado di raggiungere gli endpoint http://localhost:8082/node/peers e creare ed elencare i peer.

Ecco dove le cose si complicano. Abbiamo bisogno di più di un nodo in esecuzione e abbiamo bisogno che i nodi rispondano ai messaggi dopo l'aggiunta di transazioni o blocchi. Abbiamo anche bisogno che i nodi parlino tra loro durante l'avvio o l'aggiunta di nodi. vado a copiare SimpleNodeService ed SimpleNodeController a NodeService ed NodeController. Lascerò le vecchie classi in giro nel caso tu stia guardando il codice su GitHub e desideri seguire, ma commenterò il Component ed RestController annotazioni in modo che non vengano avviate in fase di esecuzione.

Aggiungerò un endpoint aggiuntivo al file NodeController per consentire la conferma che una transazione è diventata un blocco in tutti i nodi:

@GetMapping(percorso = "transazioni/:transactionId/conferme",
produce = MediaType.ALL_VALUE)
Mono getTransactionFromNode(
@RequestParam("transactionId") String TransactionId) {
restituisce nodeService
.getConferme(Long.valore di(ID transazione))
.map(b -> b.toString());
}

Ciò significa che ho bisogno di una nuova serie di metodi in NodeService per ottenere le conferme da tutti i nodi:

Mono pubblico getConfirmation(Node peer, long transactionId) {
URL stringa = peer.getUrl()
+ "/blocco/blocchi/transazioni/"
+ ID transazione;
ceppo.info("Ricevere transazioni da: {}", URL);
cliente di ritorno
.ottenere()
.uri(URL)
.retrieve().bodyToMono(Block.class);
.onErrorContinue((t, o) -> Mono.vuoto());
}

Mono getConfirmations(long transactionId) {
// Ottieni il conteggio dei peer con la conferma dell'esistenza della transazione
blockchain di ritorno
.findTransactionInChain(transactionId, blockchain
.getAllBlocks())
.zipWith(peers.findAll()
.flatMap(peer -> getConfirmation(peer,
transazioneId)))
.contare();
}

Questo restituirà un conteggio dei nodi che hanno questa transazione in un blocco. Ma prima, devo creare un nuovo endpoint in BlockController, uno che segnalerà se la transazione è in un blocco nella blockchain.

@GetMapping(percorso = "blocchi/transazione/{transactionId}", 
produce = MediaType.APPLICATION_JSON_VALUE)
Mono pubblico getTransaction(
@PathVariable("transactionId") TransactionId lungo) {
blockchain di ritorno
.findTransactionInChain(transactionId,
blockchain.getAllBlocks())
.last() // presuppone che ce ne sia solo uno
.switchIfEmpty(Mono.errore(nuova ResponseStatusException(
Stato HTTP.NON TROVATO,
"Transazione non trovata in Blockchain")));

Fortunatamente, abbiamo già un metodo findTransactionInChain che restituirà il blocco in cui si trova una transazione.

Successivamente, dobbiamo rispondere ai messaggi di Kafka. Aggiungeremo un messageHandler metodo che trasmetterà messaggi a tutti i nodi peer:

Mono pubblico messageHandler(Messaggio m) {
if("Blocco aggiunto".equals(m.getMessage())) {
restituisce peers.findAll()
.flatMap(p -> sendLatestBlock(p, m.getBlock()))
.switchIfEmpty(Mono.just(m.getBlock()))
.Ultimo();
} else if("addedTransaction".equals(m.getMessage())) {
restituisce peers.findAll()
.flatMap(p -> sendTransaction(p, m.getTransaction()))
.switchIfEmpty(Mono.just(m.getTransaction()))
.Ultimo();
} else if("getBlocks".equals(m.getMessage())) {
restituisce peers.findAll()
.flatMap(p -> getBlocks(p))
.switchIfEmpty(blockchain.getLastBlock())
.Ultimo();
} Else {
log.error("messaggio sconosciuto {}", m);
return Mono.vuoto();
}
}

Ciò richiede due nuovi metodi per effettuare richieste ad altri nodi:

Mono pubblico sendLatestBlock(Nodo peer, 
Blocco blocco) {
Stringa URL = peer.getUrl() + "/block/blocks/latest";
ceppo.info("Pubblicazione dell'ultimo blocco in: {}", URL);
cliente di ritorno
.mettere()
.uri(URL)
.body(blocco, Block.class)
.scambio();
}

Mono pubblico sendTransaction(Nodo peer,
transazione di transazione) {
Stringa URL = peer.getUrl() + "/transazione";
ceppo.info("Invio transazione '{}' a: {}", transazione, URL);
cliente di ritorno
.inviare()
.uri(URL)
.body(transazione, Transaction.class)
.scambio();
}

Abbiamo già il POST al /transaction endpoint, ma dobbiamo aggiungere il PUT a /block/blocks/latest punto finale.

@PutMapping(percorso = "/blocco/blocchi/ultimo", 
produce = MediaType.APPLICATION_JSON_VALUE)
Mono pubblico checkReceivedBlocco(
@RichiestaBlocco corpo ricevutoBlocco) {
restituisce blockchain.checkReceivedBlock(receivedBlock);
}

Ciò richiede un nuovo metodo nel Blockchain servizio.

Mono pubblico checkReceivedBlock(Blocco ricevutoBlocco) {
restituisce getLastBlock()
.filter(b -> b.getIndex() < ricevutoBlock.getIndex())
.flatMap(b -> {
log.info(
"Blockchain forse dietro. Abbiamo: {}, Peer ha: {}",
b.getIndex(),
ricevutoBlock.getIndex());
if (b.getHash().equals(receivedBlock.getPreviousHash())) {
log.info("Aggiungere il blocco ricevuto alla nostra catena");
return addBlock(receivedBlock, true);
} Else {
log.info("Interrogazione della catena dai nostri colleghi");
emettitore.send(TOPIC, Message.builder()
.type("vuoto")
.message("getBlocks")
.costruire())
.sottoscrivi();
return Mono.vuoto();
}
});
}

Puoi vedere come il livello del nodo comunica con altri nodi tramite l'API pubblica. C'è un problema però. Ogni nodo è rappresentato da un singolo URL, ma dobbiamo parlare con due servizi separati: il servizio di blocco e il servizio di nodo. Farò un semplice ingresso con un'istanza Nginx. In questo modo possiamo parlare con entrambi i servizi (e altri in seguito) con un unico URL. Puoi guardare il codice su GitHub per i dettagli sulla configurazione di Nginx e sull'aggiunta di tutti i servizi al file docker-compose-node1.yaml.

Avviando tutto, tutti gli endpoint funzionano ancora e posso vedere nei log la comunicazione tra il servizio blockchain e il servizio nodo sul bus Kafka. Ora è il momento di creare un secondo nodo. Copia il docker-compose-node1.yaml a docker-compose-node2.yaml e cambiare la porta esterna del servizio Nginx da 8080 a 8081 per raggiungere il nodo 1 alla porta 8080 e il nodo 2 alla porta 8081. Creerò anche un piccolo script chiamato startnode1 per avviare ogni servizio in ordine ed emettere i log dal servizio del nodo:

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 nodo1 -f docker-compose-node1.yaml up -d mongo-express
docker compose -p nodo1 -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 su blockchainnode

Perché l'ultima riga non ha a -d flag, mostra i log finché Ctrl-C non lo interrompe. io uso il -p node1 flag in modo da poter creare istanze separate dei servizi. Quindi, copia in un file chiamato startnode2, ma sostituisci il file di composizione Docker per avviare il nodo 2 e il file -p flag a node2. Non dimenticare di impostare il flag eseguibile su ciascuno:

chmod +x startnode1
chmod +x startnode2

C'è un ultimo cambiamento. Il membro me stesso del servizio del nodo deve avere l'URL visto da altri servizi, quindi l'utilizzo dell'host locale non funzionerà. Ho impostato una proprietà Spring in application.properties:

server.me stesso: http://localhost:8080

Quindi lo sovrascrivo in docker-compose-node1.yaml che ora assomiglia a questo:

versione: '3.1'
servizi:
mongo:
immagine: mongo
riavvia: sempre
porti:
- 27017
ambiente:
MONGO_INITDB_ROOT_USERNAME: radice
MONGO_INITDB_ROOT_PASSWORD: esempio
guardiano dello zoo:
immagine: confluentinc/cp-zookeeper:latest
porti:
- 2181
ambiente:
ZOOKEEPER_CLIENT_PORT: 2181
cafka:
immagine: confluentinc/cp-kafka:latest
porti:
- 9092
- 29092
sinistra:
- guardiano dello zoo
ambiente:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: guardiano dello zoo:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT: PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: TESTO PLAIN
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
blockchain:
immagine: rlkamradt/blockchain:1.0-SNAPSHOT
porti:
- 8080
ambiente:
SERVER_MIO: http://192.168.0.174:8080
MONGO_HOST: mongo
SPRING_KAFKA_BOOTSTRAP-SERVER: kafka:29092
nodo blockchain:
immagine: rlkamradt/blockchainnode:1.0-SNAPSHOT
porti:
- 8080
ambiente:
SERVER_MIO: http://192.168.0.174:8080
MONGO_HOST: mongo
SPRING_KAFKA_BOOTSTRAP-SERVER: kafka:29092
nginx:
immagine: nginx:ultimo
volumi:
- ./nginx.conf:/etc/nginx/nginx.conf
porti:
- 8080: 80

Il docker-compose-node2.yaml cambia anche la porta in 8081 per entrambi i SERVER_MYSELF e la nginx.ports valore.

Avvia entrambe le istanze. Quando sono entrambi in esecuzione, puoi connetterti l'uno all'altro:

Ora puoi creare transazioni e minare blocchi, come mostrato in precedente articolo, ma puoi elencare le transazioni e i blocchi in entrambi i nodi. Il protocollo peer-to-peer garantisce che entrambi i nodi dispongano degli stessi dati.

Non dico che sia perfetto a questo punto. Ci sono molte sequenze diverse che devono essere testate per garantire che, indipendentemente da come vengono fatte le cose, la blockchain nelle diverse istanze rimane la stessa. Ma questo articolo è già abbastanza lungo e sono sicuro che non vuoi leggere di me che eseguo il debug di questo web!

Grazie per aver letto questo articolo piuttosto lungo, ho cercato di condensarlo per renderlo il più conciso possibile ma è un argomento molto complicato.

Penso che il prossimo articolo di questa serie sarà un po' più semplice. Discuteremo la parte finale del puzzle: minatori e utenti.

Tutto il codice per questo articolo può essere trovato qui:

Il precedente articolo di questa serie:

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

Timestamp:

Di più da Medio