Na série IPFS com o Nabu, o CID (Content Identifier) é permanente: os mesmos bytes, o mesmo nome. Este post é o nome que pode passar a apontar para outro CID.
A proposta
Um site, um perfil ou a cabeça de um feed mudam. Publicar de novo gera outro CID. Quem já guardou o CID antigo continua no conteúdo antigo, que é o combinado do endereçamento por conteúdo. Falta um nome estável, controlado por alguém, que diga "a versão atual é este CID".
A proposta do IPNS (InterPlanetary Name System, o sistema de nomes interplanetário) é esse ponteiro. O nome não muda. O valor muda, e a mudança é assinada por quem tem a chave.
A Demonstração
A chave do registro é o PeerId de quem publica, prefixada pelos bytes de /ipns/. O valor é um registro assinado pela chave privada correspondente. Quem quiser verifica a assinatura sem perguntar a um servidor. O registro traz o valor (em geral o caminho ou o CID atual, em bytes), um sequence, um prazo de validade e um TTL (Time To Live, por quanto tempo uma cópia pode ser reutilizada sem perguntar de novo).
O sequence é um contador. IpnsRecord.compareTo coloca o sequence maior depois do menor. No empate, o registro cuja validade acaba antes vem primeiro. Se a validade deste não acaba antes da do outro, a comparação devolve zero. resolveValue ordena a lista e fica com o último, portanto com o sequence mais alto. Um registro vencido deixa de ser a resposta na validação do prazo. Como o CID do conteúdo continua imutável, a história antiga segue endereçável para quem guardou o CID. O IPNS só move o ponteiro.
A publicação usa a mesma DHT (Distributed Hash Table) do post anterior. O registro é gravado nos pares mais próximos da chave /ipns/ mais o PeerId. Resolver é perguntar a esses pares e ficar com o registro de maior sequence que ainda valida.
No Nabu
IPNS.getKey monta a chave: escreve os bytes de /ipns/ e em seguida os bytes do multihash do publicador. createSignedRecord, em src/main/java/org/peergos/protocol/ipns/IPNS.java, monta o protobuf IpnsEntry. O corpo assinado é um mapa CBOR (Concise Binary Object Representation) com as chaves TTL, Value, Sequence, Validity e ValidityType. A validade é uma data no formato RFC 3339 com nanossegundos, em UTC, produzida por formatExpiry. A assinatura cobre o prefixo ASCII ipns-signature: mais esses bytes CBOR, e vai no campo SignatureV2. Para Ed25519 a chave pública já está no PeerId, e o registro não repete pubKey. Para outros tipos, o registro inclui a pública em protobuf. Dado extra da aplicação entra no mapa com o prefixo _ mais o sufixo que o chamador passou. O tamanho máximo aceito na validação é MAX_RECORD_SIZE, 10 KiB. Entrada maior, ou sem SignatureV2 e sem Data, é descartada por parseAndValidateIpnsEntry.
EmbeddedIpfs.publishValue é o atalho da aplicação. O publicador é o multihash do PeerId derivado da chave privada. O prazo é LocalDateTime.now() mais hoursTtl. O TTL em nanossegundos é hoursTtl vezes 3.600.000.000.000. O método assina e chama a DHT:
byte[] signedRecord = IPNS.createSignedRecord(value, expiry, sequence, ttlNanos, extraDataKeySuffix, extraData, priv);
return dht.publishValue(pub, signedRecord, node);
O inteiro devolvido é quantos pares guardaram o registro. Há também publishPresignedRecord, para quem já traz os bytes assinados. resolveValue pede resolveRecords e devolve o value do último item depois de sorted(). IpnsRecord.compareTo ordena pelo sequence. O último é o mais novo. Lista vazia vira o erro Couldn't resolve IPNS value.
Kademlia.publishValue valida antes de espalhar. parseAndValidateIpnsEntry precisa devolver um registro, e o publicador desse registro precisa ser a chave pedida. Se já existe um registro local e o novo não é maior na comparação, o antigo permanece. A busca mira pelo menos 30 pares próximos da chave (minPublishes), com 3 consultas em paralelo (queryParallelism). A chave de roteamento é o SHA-256 dos bytes /ipns/ mais o publicador, não o PeerId cru.
No processo standalone o store é DatabaseRecordStore, H2, no caminho datastore/h2-v2.datastore dentro do diretório IPFS. A tabela se chama records. O valor cabe em 10 KiB (SIZE_OF_VAL), o mesmo teto do registro, e o PeerId em 100 bytes na coluna. RamRecordStore existe para testes e para os exemplos, que não precisam do registro depois que o processo morre. A interface é RecordStore: ler, gravar e fechar. EmbeddedIpfs.stop fecha o store.
O que fica de fora: o Nabu não cria uma chave IPNS separada do PeerId por conta própria. A chave de publicação é a chave que a aplicação passa, e no fluxo de publishValue ela é a identidade cujo PeerId entra na chave /ipns/. Não há resolução IPNS por DNS. Um nome ipns:// no navegador fica com o cliente. A API HTTP expõe ipns/get, descrito no post da API, e não expõe publicação.
Nesta série
O anterior é A DHT Kademlia. O próximo é Onde o bloco mora.
Top comments (0)