WEB3DEV

Cover image for Escreva aplicativos descentralizados em JavaScript — Libp2p Basics
Diogo Jorge
Diogo Jorge

Posted on • Atualizado em

Escreva aplicativos descentralizados em JavaScript — Libp2p Basics

Este artigo foi escrito por Laszlo Fazekas e traduzido por Diogo Jorge. O original em inglês pode ser encontrado aqui.

Fonte: https://discuss.libp2p.io/

Libp2p é uma das bibliotecas p2p mais conhecidas. É a base do Interplanetary Filesystem e será a base do Ethereum 2.0 , por isso é uma escolha muito boa se você deseja desenvolver aplicativos descentralizados.

Neste artigo, mostrarei as concepções básicas da libp2p e desenvolveremos um aplicativo de bate-papo minimalista de navegador (sem necessidade de servidor próprio) usando a implementação JavaScript de libp2p e vue.js.

PeerId

PeerId é um dos principais elementos da libp2p. É algo como um endereço BitCoin ou Ethereum que é gerado a partir da parte da chave pública de um par de chaves. Como um endereço BitCoin, ele é único e pode ser usado para assinar uma mensagem pela chave privada que pode ser validada pela chave pública associada ao peerId. Você pode gerar um peerId pelo utilitário peer-id.

Você pode simplesmente instalar o peer-id pelo npm:

sudo npm i peer-id -g
Enter fullscreen mode Exit fullscreen mode

Após a instalação, você pode simplesmente gerar seu próprio peer-id pela ferramenta peer-id CLI:

peer-id
{
"id": "QmPMKmmRyepsh4Wq24msEdeGJQWSszy7eWNSjL53bxz3sS",
"privKey":"CAASqAkwggSkAgEAAoIBAQC6/4DQXtW9HYbx5wPNbcsNbwMITH8Mg70O+zhQ9t9w/9Z7BuOzwQqu/ABaRclkl1FIuGfIayKSzqkczYkdY3I6bMUtn8dyHuJi1JzJj5PtaGfK81ra6hNvkVRqttYKJdgpPdGWCa6ouRbFPf3l0w2sK0ejf1Im/h4bAL1ltgZK7G+fXZTlUdAoEvQJja3bl9b47zqWiq+oVaez7uBhemrJQ6Ao09Tlr/RpVfbsc48NAczuDqh9nZzUq27LQEu9OXOf6xk/hPaDcof0ubQs9REXDCcDq01Cg6dQ5odKKHcT9hTeytZlvzfZZgleU8pME9r1EVpG5aEMn5qC0zfoe3PTAgMBAAECggEAWBoa+ZFEyHYJ5xy9WOMaoLilyBoqXZ4Py+gmj1bQzS9sQMhtLXqM6waFsAJjMUZtoIJpOy7muh4t5QkdScBZyBcJC0bVM/pDFOcw+3Hu8xKWnDLtomhYQd9J04FS9LMB1eRvQ25KYOnbRZDAd7BpJ624cdqBvSdKzdQaZ7pL2q4d/P1DUVLVfNDHzHlHyiLoxL1Ilm5YQtGGA3PwTLmSMqWGPvrzK4n0qL2rXUMbio0wf7w3lWDnBNQpncwdcPqUSTPDItwa2GNImH5yZRIF/vP2Qdky7LvNoxIzr+UhyQ6s2YSfRLwgnSPNL4IRuRaUWCtFprdaMd/x65xkBDVP2QKBgQDqXV922hbpMWgGPUco0uv7C8UApWiA/TP95pDzJuoD/SL0pn15QQrPMYTbOf32DrwC/sVMuuIbStqJd8RSApTsQO48KiPgmJM6WgD35umlYpmPQh8djvhPTe3nCadoxy+40i7NuuDde9AtvDlqwti4t1E/6lnYNZecbrXnT3ELZwKBgQDMQryboWcrUS+pfrJADtNiBOBtAAIQz7aHZRmk9ZFY7L8ekSB1Cuee4/FXswkrie+jE1rGbV97OQ1f6qW10SkoN64OIs8AVx408MxLdEejLg0vLwjEqEVXE2Wc1/TkOuEOpsrniKgisIZThrdC2RhmOqtaKRnAWz761ypCluP8tQKBgQCzqzV+Zh9eUpQPBHdDIr/qS9GRdz0wdeyf31yMK+8Hc86Sg/h5NpXU1X+mmUTKl+0m1q3m7vZcOfxjmr+Up4oHvJdm5F9w1Uc5WrqXUh0Yvwg+PVChVnOiSHnzvwDqYJmDNQ7QhU3SPhMQnNXftNR0d4UAXObXy+4Y7P7i/5IITQKBgQCtIxh6Frq7lep/kjwHXknA+P8+hVY658YBSCoPkHOuW6a4gy1u6FpibTZCLyjjtdzhbuNv9H+NlFOI7P2fevaW93Na2hh6Yl3hZAbXIm4inENirkyRXUzBPVjRNKCI4HuqDqlIzqYuGVES9crbJ+etp6ddGh+Q1AczWjShEwOXTQKBgF2cuW8w7ghAXapb2BKQU9bZTMAevohw6cxEmajNngVGSGMQJIJdhEoaGUXdtBOpOxAfYdycegf9VkPraVERXAbCMDk5mAh4Aor1SsHUDnQ7VKMLQfGzH3Jfk2TpXqy/11rJlLAl05LCyAeHGLVM4CQrvpLOIeM5n6j/3QWO3Jdh",
"pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6/4DQXtW9HYbx5wPNbcsNbwMITH8Mg70O+zhQ9t9w/9Z7BuOzwQqu/ABaRclkl1FIuGfIayKSzqkczYkdY3I6bMUtn8dyHuJi1JzJj5PtaGfK81ra6hNvkVRqttYKJdgpPdGWCa6ouRbFPf3l0w2sK0ejf1Im/h4bAL1ltgZK7G+fXZTlUdAoEvQJja3bl9b47zqWiq+oVaez7uBhemrJQ6Ao09Tlr/RpVfbsc48NAczuDqh9nZzUq27LQEu9OXOf6xk/hPaDcof0ubQs9REXDCcDq01Cg6dQ5odKKHcT9hTeytZlvzfZZgleU8pME9r1EVpG5aEMn5qC0zfoe3PTAgMBAAE="
}
Enter fullscreen mode Exit fullscreen mode

Chamar o peer-id gerará um JSON com a chave pública e privada, e o id que é uma chave pública multihash. Este id é o seu endereço de par exclusivo.

Multihash

Multihash é um formato de hashing à prova de futuro que é usado por libp2p e IPFS. É construído a partir de duas partes. Os primeiros 2 caracteres são o cabeçalho de hash que define o algoritmo de hash e a segunda parte é o próprio hash. No exemplo acima (na parte id do peerId JSON) Qm significa que é um hash _SHA-256 e a segunda parte é o _hash de 256 bits codificado em base58.

Multiendereço

Multiaddress é algo como um URL HTTP, mas é mais abrangente. Um multiaddr é uma lista de pares de protocolo/valor que podem apontar para um host, um serviço, um par, etc. Vejamos alguns exemplos:

/ip4/1.2.3.4 — este multiaddr aponta para um host

/ip4/1.2.3.4/tcp/80 — aponta para a porta TCP 80 do host

/dns4/example.com/udp/123 — direciona para a porta UDP 123 de example.com

/p2p/QmbEKwwsuzArDenmWJfcFgyQ8uATQrmkjAsaT8VaGocV1x — isso direciona para um peerId

/ip4/7.7.7.7/tcp/4242/p2p/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N — este multiaddr direciona exatamente para um host e uma porta TCP onde podemos nos comunicar com o par fornecido

Agora que sabemos o básico, vamos avançar para a implementação. Libp2p é um stack de rede modular com muitas implementações. Neste artigo, usarei a implementação de JavaScript que pode ser executada no server-side em node.js ou em seu navegador. Vamos ver algum código para entender como o js-libp2p funciona:

'use strict'
const Libp2p = require('libp2p')
const TCP = require('libp2p-tcp')
const { NOISE } = require('@chainsafe/libp2p-noise')
const createNode = async () => {
 const node = await Libp2p.create({
   address: {
     listen: ['/ip4/0.0.0.0/tcp/4321']
   },
   modules: {
     transport: [ TCP ],
     connEncryption: [ NOISE ]
   }
 })
 await node.start ()
 Return node
}
Enter fullscreen mode Exit fullscreen mode

O exemplo acima mostra como podemos iniciar um nó libp2p. O método Libp2p.create tem duas opções aqui. O primeiro é o endereço onde o nó irá escutar. Nesse caso, o nó escutará em cada endereço IP e na porta 4321. A segunda opção são as definições dos módulos. Libp2p é um stack muito modular. Cada módulo é definido por uma interface, e você pode escolher livremente qual usar ou pode criar seus próprios módulos. Cada parte do stack é modular. Você pode definir a camada de transporte, as criptografias, você pode definir seu próprio mecanismo de descoberta de pares, etc. Neste caso, usaremos transporte TCP e criptografia NOISE.

O código acima funciona bem em node.js, mas em um navegador da Web, não é possível abrir portas de servidor. Para resolver este problema, a libp2p pode usar servidores de retransmissão que podem abrir as portas de servidor necessárias e encaminhar o tráfego para o navegador.

this.libp2p = await Libp2p.create({
 address: {
   listen: [
    "/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star",
    "/dns4/wrtc-star2 .sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star",
   ],
 },
 modules: {
   transport: [Websockets, WebRTCStar],
   connEncryption: [NOISE],
   streamMuxer: [Mplex],
   peerDiscovery: [ Bootstrap],
   dht: KadDHT,
 },
 config: {
   peerDiscovery: {
     [Bootstrap.tag]: {
     enabled: true,
     list: [
"/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmZa1sAxajnQjVM8WjWXoMbmPd7NsWhfKsPkErzpm9wGkp",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
"/dnsaddr/bootstrap .libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
     ],
     },
   },
   dht: {
     enable: true,
   },
 },
});
Enter fullscreen mode Exit fullscreen mode

Este exemplo mais complexo usa os servidores wrtc-star1.par.dwebops.pub e wrtc-star2.sjc.dwebops.pub como retransmissões por meio do protocolo webrtc. Esses servidores são fornecidos pela comunidade libp2p apenas para teste. Se você estiver construindo um aplicativo do mundo real, deverá instalar seus próprios servidores webrtc star.

Outra novidade são os nós de inicialização (bootstrap nodes). Esses nós são o ponto de partida da descoberta de pares. Quando seu nó for iniciado, ele se conectará a esses nós primeiro. Após a conexão, nosso nó obterá outros endereços de nós que também estão conectados aos nós de inicialização, e conectará alguns deles, portanto, após a inicialização, os nós de inicialização não são mais necessários, a rede p2p é construída automaticamente. Os nós de inicialização no exemplo também são fornecidos pela comunidade libp2p. Ao definir diferentes nós de inicialização, você pode definir suas próprias redes p2p separadas.

A última coisa que eu quero falar é o DHT. Distributed Hash Table é um armazenamento de chave/valor distribuído que é armazenado nos nós. Esse armazenamento de chave/valor é usado para armazenar o endereço de Internet atual do peerId. É algo como o sistema DNS, onde os servidores DNS armazenam os endereços IP dosdomínios. O DNS também é distribuído como o DHT, mas neste caso, usamos o peerId em vez do domínio e o multiendereço do endpoint em vez do IP.

let peerId = PeerId.parse(this.otherPeerId);
deixe resultado = aguarde this.libp2p.peerRouting.findPeer(peerId);
this.otherPeerMultiaddrs = result.multiaddrs;
Enter fullscreen mode Exit fullscreen mode

O exemplo acima mostra como você pode encontrar um par por seu peerId. O método findPeer do módulo peerRouting tenta descobrir o par pelo peerId e, se encontrá-lo, o multiaddress estará disponível no resultado.

Agora podemos iniciar nossos próprios nós no navegador e encontrar outros pares. O próximo passo é se comunicar com eles.

Em primeiro lugar, temos que definir um protocolo e lidar com as conexões de entrada. É algo como abrir uma porta de servidor em um aplicativo cliente-servidor tradicional.

this.libp2p.handle('/chat/1.0.0',
 ({ connection, stream, protocol }) => {
   this.remotePeerId = connection.remoteAddr.getPeerId();
   pipe(
     stream,
     (source) => {
       return (async function* () {
        for await (const buf of source)
          yield array2str(buf.slice());
       })();
     },
     async (source) => {
      for await (const msg of source) {
       this.messages.push("> " + msg);
      }
     }
   );
});
Enter fullscreen mode Exit fullscreen mode

O código acima define o protocolo 'chat/1.0.0' (este é o formato padrão de um protocolo) e manipula as conexões de entrada para ele. A função do manipulador tem 3 parâmetros onde o 2º é o mais importante, o fluxo de entrada. Fluxos de JavaScript são coisas muito complicadas, mas espero que existam algumas ferramentas boas para lidar com eles. Neste exemplo, estamos usando a função pipe. O primeiro parâmetro do pipe é um fluxo, e o último é um consumidor. Os parâmetros do meio podem ser transformadores de fluxo. No nosso caso, transformamos os elementos do fluxo pela função array2str porque o fluxo é binário e precisamos de strings UTF8 simples. A última função (o consumidor) simplesmente lê as mensagens do fluxo e as grava.

Agora temos nós no navegador que podem encontrar uns aos outros e temos um manipulador de protocolo de bate-papo. A última etapa é conectar-se de um ponto a outro e enviar algumas mensagens no protocolo de bate-papo.

const { stream, protocol } = await this.libp2p.dialProtocol(
 peerId, chatProtocol
);
this.chatQueue = pushable();
pipe(
 this.chatQueue,
 (source) => {
   return (async function* () {
     for await (const msg of source) yield str2array(msg);
   })();
 },
 stream
);
Enter fullscreen mode Exit fullscreen mode

Libp2p tem um método dialProtocol para conectar outros pares. O método tem 2 parâmetros. O peerId de destino e o identificador de protocolo. O resultado do método é um fluxo, então usamos a função pipe novamente. O primeiro parâmetro do pipe é um pushable. Pushable é uma fila que pode ser lida como um fluxo. Vamos enviar as mensagens de chat para esta fila. O segundo estágio do pipe é um transformador que chama str2array para converter as mensagens de string em formato binário. O último estágio do pipe é o próprio fluxo.

Vamos ver o código completo. Eu usei o Vue, porque ele é compacto e fácil de ler.

<template>
  <div>
    <p>Your peerId: {{ myPeerId }}</p>
    <p>
      Other peerId:
      <input type="text" style="width: 420px" v-model="otherPeerId" /><button
        @click="findOtherPeer"
      >
        Find
      </button>
    </p>
    <p v-for="(multiaddr, idx) in otherPeerMultiaddrs" :key="'ma_' + idx">
      Other peer multiaddr: {{ multiaddr.toString() }}
    </p>
    <p v-for="(protocol, idx) in otherPeerProtocols" :key="'p_' + idx">
      Other peer protocol: {{ protocol }}
    </p>
    <div v-if="otherPeerMultiaddrs.length > 0 && otherPeerProtocols.length > 0">
      <p>
        {{ otherPeerMultiaddr }}{{ otherPeerProtocol }}
        <button @click="dialProtocol">Dial protocol</button>
      </p>
    </div>
    <p v-if="remotePeerId">
      Remote peer connected: {{ remotePeerId.toString() }}
    </p>
    <p v-for="(msg, idx) in messages" :key="'msg_' + idx">
      {{ msg }}
    </p>
    <div v-if="chatQueue">
      <input type="text" style="width: 600px" v-model="chatMessage" /><button
        @click="sendMessage"
      >
        Send message
      </button>
    </div>
  </div>
</template>

<script lang="ts">
import "babel-polyfill";
import Libp2p from "libp2p";
import Websockets from "libp2p-websockets";
import WebRTCStar from "libp2p-webrtc-star";
import { NOISE } from "libp2p-noise";
import Mplex from "libp2p-mplex";
import Bootstrap from "libp2p-bootstrap";
import KadDHT from "libp2p-kad-dht";
import PeerId from "peer-id";
import pushable from "it-pushable";
import pipe from "it-pipe";
import { array2str, str2array } from "./utils";
import { Component, Vue } from "vue-property-decorator";
const chatProtocol = "/chat/1.0.0";
@Component
export default class App extends Vue {
  private libp2p: Libp2p;
  private myPeerId: string = "";
  private otherPeerId: string = "";
  private otherPeerMultiaddrs: any[] = [];
  private otherPeerProtocols: string[] = [];
  private otherPeerMultiaddr: string = "";
  private otherPeerProtocol: string = "";
  private remotePeerId: any = "";
  private chatMessage: string = "";
  private messages: string[] = [];
  private chatQueue: any = false;
  async init() {
    this.libp2p = await Libp2p.create({
      addresses: {
        listen: [
          "/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star",
          "/dns4/wrtc-star2.sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star",
        ],
      },
      modules: {
        transport: [Websockets, WebRTCStar],
        connEncryption: [NOISE],
        streamMuxer: [Mplex],
        peerDiscovery: [Bootstrap],
        dht: KadDHT,
      },
      config: {
        peerDiscovery: {
          [Bootstrap.tag]: {
            enabled: true,
            list: [
              "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
              "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
              "/dnsaddr/bootstrap.libp2p.io/p2p/QmZa1sAxajnQjVM8WjWXoMbmPd7NsWhfKsPkErzpm9wGkp",
              "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
              "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
            ],
          },
        },
        dht: {
          enabled: true,
        },
      },
    });
    await this.libp2p.start();
    this.myPeerId = this.libp2p.peerId.toB58String();
    this.libp2p.handle(chatProtocol, ({ connection, stream, protocol }) => {
      this.remotePeerId = connection.remoteAddr.getPeerId();
      pipe(
        stream,
        (source) => {
          return (async function* () {
            for await (const buf of source) yield array2str(buf.slice());
          })();
        },
        async (source) => {
          for await (const msg of source) {
            this.messages.push("> " + msg);
          }
        }
      );
    });
  }
  mounted() {
    this.init();
  }
  async findOtherPeer() {
    let peerId = PeerId.parse(this.otherPeerId);
    let result = await this.libp2p.peerRouting.findPeer(peerId);
    this.otherPeerMultiaddrs = result.multiaddrs;
    this.otherPeerProtocols = this.libp2p.peerStore.protoBook.get(peerId);
    this.otherPeerMultiaddr = this.otherPeerMultiaddrs[0];
    this.otherPeerProtocol = chatProtocol;
  }
  async dialProtocol() {
    let peerId = PeerId.parse(this.otherPeerId);
    const { stream, protocol } = await this.libp2p.dialProtocol(
      peerId,
      chatProtocol
    );
    this.chatQueue = pushable();
    pipe(
      this.chatQueue,
      (source) => {
        return (async function* () {
          for await (const msg of source) yield str2array(msg);
        })();
      },
      stream
    );
  }
  sendMessage() {
    this.chatQueue.push(this.chatMessage);
    this.messages.push("< " + this.chatMessage);
    this.chatMessage = "";
  }
}
</script>

<style>
body {
  padding: 20px;
}
</style>

// based on https://www.dreamincode.net/forums/topic/353343-how-to-convert-uint8array-to-uint16array-or-string/
export function array2str(uint8buf) {
    let buf = new ArrayBuffer(uint8buf.length);
    let bufView = new Uint16Array(buf);
    let count = 0;
    for (let i = 0; i < bufView.length; i++) {
        bufView[i] = uint8buf[count++] + (uint8buf[count++] << 8);
    }
    return String.fromCharCode.apply(null, bufView);
}

// based on str2ab from https://gist.github.com/skratchdot/e095036fad80597f1c1a
export function str2array(str) {
    let buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char
    let bufView = new Uint16Array(buf);
    for (var i = 0, strLen = str.length; i < strLen; i++) {
        bufView[i] = str.charCodeAt(i);
    }
    return new Uint8Array(bufView.buffer, bufView.byteOffset, bufView.byteLength);
}
Enter fullscreen mode Exit fullscreen mode

Você pode testá-lo online em https://thebojda.github.io/js-libp2p-browser-chat/index.html . Abra este URL em 2 abas do navegador. Copie o peerId de uma guia para a outra guia e pressione o botão 'Localizar'. Se tudo correr bem, o nó do navegador encontrará o outro nó na outra guia. Em seguida, pressione o botão 'Protocolo de discagem' e você pode começar a conversar entre as duas guias.

Libp2p é a base de muitos aplicativos p2p. Espero que esta breve introdução o ajude a começar a desenvolver seus próprios aplicativos p2p (talvez um novo aplicativo de economia compartilhada ou seu próprio blockchain). Então, vamos começar a codificar…

Mais conteúdo em plainenglish.io

Top comments (0)