DEV Community

Pedro Pietroluongo
Pedro Pietroluongo

Posted on Edited on

A DHT Kademlia

Na série IPFS com o Nabu, o Bitswap já sabe pedir um bloco a um par conhecido. Este post é como achar esse par quando ninguém passou o PeerId (o identificador do par).

A proposta

Não existe um servidor central com a lista "quem tem este CID" (Content Identifier, o identificador do conteúdo). Cada nó só conhece uma fatia dos outros. A proposta da DHT (Distributed Hash Table, tabela de hash distribuída) é uma resposta que caiba nessa fatia e, ainda assim, chegue perto de quem publicou o anúncio.

Kademlia é o formato dessa tabela no IPFS. A mesma estrutura serve para achar um par a partir do PeerId e para guardar os registros IPNS (InterPlanetary Name System) do próximo post.

A Demonstração

Kademlia organiza os pares por distância XOR (exclusive or, o ou exclusivo bit a bit). A chave de um par é o hash do PeerId, 256 bits. A distância entre duas chaves é o XOR dos bits. Quanto mais cedo o primeiro bit diferente, mais distantes. Duas chaves que só diferem no último bit são vizinhas. Duas que diferem no primeiro bit estão em metades opostas do espaço.

Cada nó guarda uma tabela de roteamento: k-buckets, baldes de pares, um balde para cada faixa de distância. O nó conhece muita gente perto de si e poucos representantes de cada região longe. Com isso, qualquer chave é alcançável em poucos saltos. Cada salto pergunta a alguém mais próximo da chave do que o nó atual.

Publicar um provedor é escrever, nos nós mais próximos da chave do CID, o registro "este PeerId tem este CID, nestes endereços". Procurar é perguntar, de balde em balde, pelos nós mais próximos dessa mesma chave, até alguém devolver o registro. O anúncio expira. Quem continua com o bloco anuncia de novo. Quem acabou de gravar um bloco anuncia logo, sem esperar o ciclo.

O protocolo na rede pública é /ipfs/kad/1.0.0. Existe um identificador de DHT local, /ipfs/lan/kad/1.0.0, para uma tabela que não sai da rede de casa. O nó que o EmbeddedIpfs monta fala a rede pública.

No Nabu

Kademlia em src/main/java/org/peergos/protocol/dht/Kademlia.java escolhe o identificador no construtor. localOnly verdadeiro seleciona LAN_DHT_ID. Falso seleciona WAN_DHT_ID, a constante /ipfs/kad/1.0.0. EmbeddedIpfs.build passa false, e também as flags de QUIC e TCP calculadas a partir dos endereços de escuta. setAddressBook repassa o caderno ao KademliaEngine.

A tabela mora em KademliaEngine. O roteador é criado com o hash de 256 bits do PeerId deste nó:

this.router = new Router(Id.create(ourPeerId.bareMultihash().toBytes(), 256), 2, 2, 2);
Enter fullscreen mode Exit fullscreen mode

O construtor de Router documenta os três inteiros: branchesPerLevel (quantos ramos nascem quando um k-bucket se divide), maxNodesPerBucket (quantos pares cabem no balde) e maxCacheNodesPerBucket (quantos cabem na cache do balde). Aqui os três valem 2. O identificador base tem 256 bits, e 2 divide 256, que é a condição que o construtor exige para a árvore fechar no último nível.

Os provedores ficam num RamProviderStore de 10.000 entradas, criado em EmbeddedIpfs.build e entregue ao engine junto com o RecordStore de IPNS e o Optional do blockstore. Reiniciar esquece quem anunciou o quê para este nó. getProviders lê dessa memória. Os registros IPNS usam o outro store, o do post seguinte.

bootstrapRoutingTable resolve dnsaddr (o multiaddr que ainda é um nome DNS), grava os endereços no caderno e disca, com 5 segundos de timeout por par. bootstrap então preenche a tabela a partir desses vizinhos. BOOTSTRAP_PERIOD_MILLIS vale 300.000, e a thread de startBootstrapThread repete o ciclo a cada esse intervalo.

findProviders(Multihash block, Host us, int desiredCount) é a busca. BitswapBlockService só a chama quando a aplicação não passou PeerIds. Nesse caso o serviço primeiro pergunta aos pares já conectados, com espera de 10 segundos. O que faltar segue para findProviders do primeiro CID da lista, pedindo até 5 provedores, e o Bitswap busca o restante nesses pares. Se a aplicação passou PeerIds, a DHT não entra. É o caminho mais rápido que o README descreve. EmbeddedIpfs.getBlocks recebe o conjunto e o repassa ao BlockService. A API dht/findprovs chama o mesmo método com num-providers, padrão 20.

Quem deve anunciar passa por ProvidingBlockstore. Se provideBlocks é verdadeiro, EmbeddedIpfs.build embrulha o blockstore. O put grava no fundo e, no thenApply, coloca o CID em toPublish, uma LinkedBlockingDeque. O construtor de EmbeddedIpfs cria o PeriodicBlockProvider com essa fila, o intervalo 22 * 3600_000 milissegundos (22 horas) e um fornecedor que chama blockstore.refs(false). start liga duas threads, CidReprovider e NewCidProvider. A primeira percorre os CIDs já guardados e republica. A segunda publica o que acabou de entrar na fila. As duas só existem quando provideBlocks é verdadeiro.

Na conexão, o handler de identify do HostBuilder abre um stream Kademlia com um par novo que anuncia /ipfs/kad/1.0.0. A condição é newPeer: o caderno estava vazio antes do identify. Assim a tabela cresce também com quem chega, não só com o bootstrap.

O que fica de fora: a DHT do Nabu não é um banco de blocos. Ela guarda ponteiros para pares e registros IPNS. Os bytes continuam no Bitswap. O store de provedores é memória. Não há persistência de provider records entre reinícios.

Nesta série

O anterior é Bitswap, o pedido de um bloco. O próximo é IPNS, um nome que muda.

Top comments (0)