Na série IPFS com o Nabu, o nó já está na rede. Este post é a troca do bloco: como um CID (Content Identifier, o identificador do conteúdo) vira bytes.
A proposta
Saber o CID não entrega os bytes. Algum par os tem, talvez vários. Pedir o arquivo inteiro a um desconhecido é frágil: a conexão cai no meio, e o que já chegou não tem nome próprio. A proposta do IPFS é pedir bloco a bloco. Cada bloco cabe numa mensagem e se verifica sozinho, porque o CID é o hash.
O protocolo que carrega esse pedido se chama Bitswap. Os dois lados usam a mesma mensagem. Enquanto este nó pede, pode atender o pedido de quem está do outro lado.
A Demonstração
A mensagem carrega três coisas. Um want é o CID que este nó quer, com um segredo opcional. Um have, ou a ausência dele (dont-have), diz se o outro lado tem esse CID. Um block são os bytes, junto com um prefixo do CID para o receptor conferir.
O want pode ir a pares que a aplicação já nomeou. Se a lista vem vazia, o serviço pergunta primeiro a quem já está conectado e, para o que ninguém entregou em 10 segundos, pede provedores à DHT (Distributed Hash Table, a tabela distribuída de "quem tem este CID"). O post da Kademlia é essa descoberta. O Bitswap é o transporte dos bytes.
Há um gancho de autorização. Antes de colocar os bytes na resposta, o nó pergunta a um BlockRequestAuthoriser se aquele par pode ler aquele CID. A resposta negativa vira um dont-have, mesmo com o bloco no disco. O Peergos usa isso com um BAT (Block Access Token), um segredo de 32 bytes que viaja no want. O processo Nabu standalone autoriza todo mundo.
flowchart LR
want[wantCid] --> local[localBlockstore]
local -->|hit| done[hashedBlock]
local -->|miss| peers[knownPeersOrDht]
peers --> bitswap[bitswapWant]
bitswap --> auth[authoriser]
auth --> done
No Nabu
O identificador está em Bitswap, em src/main/java/org/peergos/protocol/bitswap/Bitswap.java:
public static int MAX_MESSAGE_SIZE = 2*1024*1024;
public static final String PROTOCOL_ID = "/ipfs/bitswap/1.2.0";
2 MiB é o teto de uma mensagem. O construtor que EmbeddedIpfs.build usa aceita outro identificador e outro teto. O padrão é esse protocolo e Bitswap.MAX_MESSAGE_SIZE. O quarto argumento de BitswapEngine, blockAggressivePeers, vai false nesse build. Com ele verdadeiro, allowConnection recusa PeerIds que o motor já marcou em blockedPeers.
EmbeddedIpfs.getBlocks separa o trabalho antes do protocolo. Para cada Want, blockstore.has decide a lista local ou a remota. A local é lida com get. Falha ou ausência manda o want de volta para a lista remota. A lista remota segue para BitswapBlockService.get.
Bitswap.get percorre os wants e pula CID cujo multihash é do tipo identity: nesse tipo o "hash" já são os bytes, e não há bloco a transferir. Os demais entram em engine.getWant, que devolve o future já existente se o mesmo want está em voo, ou cria um WantResult novo. sendWants empurra os pedidos. Um DownloadManager por conjunto de pares, guardado numa cache de 100 entradas, mantém o envio vivo.
BitswapBlockService é quem decide a quem pedir. Se a aplicação passou PeerIds, a chamada é direta: bitswap.get(hashes, us, peers, addToBlockstore) e cada future é esperado com join. Se a lista vem vazia, o serviço usa bitswap.getBroadcastAudience(), os pares de conexões que este nó iniciou, com espera de 10 segundos por future. Want que completou sai da lista. O que falta chama dht.findProviders só com o primeiro CID, pedindo 5 provedores, e o Bitswap busca o restante nesses PeerIds.
BitswapEngine guarda os wants locais num mapa e, ao receber uma mensagem, decide o que devolver. O trecho que importa está em src/main/java/org/peergos/protocol/bitswap/BitswapEngine.java:
if (blockPresent && authoriser.allowRead(c, sourcePeerId, auth.orElse("")).join()) {
MessageOuterClass.Message.Block blockP = MessageOuterClass.Message.Block.newBuilder()
.setPrefix(ByteString.copyFrom(prefixBytes(c)))
.setAuth(ByteString.copyFrom(ArrayOps.hexToBytes(auth.orElse(""))))
.setData(ByteString.copyFrom(store.get(c).join().get()))
.build();
store.has vem antes. Se o bloco existe e o authoriser aceita, os bytes entram na mensagem, com o prefixo do CID e o auth em bytes. Se a soma passa de maxMessageSize, o motor descarrega o que já juntou e recomeça a mensagem. Se o bloco existe e o authoriser recusa, o want vai para deniedWants (uma cache de 10.000) e a resposta é dont-have. O log diz Rejecting auth. Auth inválido repetido também é recusado, com outra linha de log.
A interface do authoriser é uma função:
CompletableFuture<Boolean> allowRead(Cid block, Cid sourceNodeId, String auth);
O Want carrega o CID e um authHex opcional. String vazia vira Optional.empty(). Na API HTTP, o parâmetro auth de block/get preenche esse campo, e peers limita a quem pedir. persist falso pede o bloco sem gravá-lo no store local. O padrão da API é gravar.
O BAT em si está em src/main/java/org/peergos/blockstore/auth/Bat.java. São 32 bytes. Comprimento diferente lança Invalid BAT length. O comentário da classe o descreve como a chave secreta de uma assinatura no estilo AWS S3 V4 (Simple Storage Service, assinatura versão 4), usada para autorizar a leitura do bloco. O processo Nabu passa o authoriser (c, p, a) -> CompletableFuture.completedFuture(true). Quem embute o nó troca essa função pela política da aplicação.
O que fica de fora: o Nabu implementa Bitswap 1.2.0 com o campo de auth que o Peergos usa. Não há um mercado de blocos nem pagamento dentro do protocolo. Um bloco maior que o teto da mensagem não cabe numa resposta.
Nesta série
O anterior é Entrar na rede. O próximo é A DHT Kademlia.
Top comments (0)