Na série IPFS com o Nabu, o CID (Content Identifier, o identificador do conteúdo) já nomeia o bloco e o Bitswap já o transporta. Este post é o lugar onde os bytes ficam quando o processo guarda uma cópia.
A proposta
Um bloco chega e precisa sobreviver ao reinício. A pergunta "eu tenho este CID?" acontece o tempo todo: a cada want do Bitswap, o motor olha o store. Ir ao disco, ou a um serviço de objetos, para cada pergunta fica caro quando a resposta quase sempre é não. O store também precisa listar os CIDs que tem, para o anúncio periódico da DHT (Distributed Hash Table), e lembrar tamanho e links, para a API (Application Programming Interface) HTTP não abrir o bloco inteiro só para medir.
A proposta é uma interface pequena, dois fundos intercambiáveis e camadas opcionais por cima: um filtro aproximado na frente e uma tabela de metadados ao lado.
A Demonstração
A interface é has, get, put, rm, refs, bloomAdd. A chave de arquivo deriva do multihash em base32, o alfabeto de 32 símbolos, sem o padding =. O diretório reparte os arquivos para uma única pasta não guardar milhões de entradas.
Dois fundos implementam o contrato. O de arquivo escreve cada bloco num .data sob blocks/. O de S3 (Simple Storage Service, o armazenamento de objetos no estilo da AWS) usa o mesmo contrato contra um bucket: uma região, um endpoint, um prefixo dentro do bucket. As credenciais podem vir do JSON (JavaScript Object Notation) de config, das variáveis AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY, ou de ~/.aws/credentials.
Por cima do fundo entra um filtro. Ele responde "não tenho" sem tocar o fundo quando o CID está fora de um conjunto aproximado. Um bloom filter é uma tabela de bits. Cada CID liga alguns bits. Se algum bit que o CID exigiria está apagado, o CID com certeza não foi inserido. Se todos estão acesos, o CID talvez esteja: outro CID pode ter acendido os mesmos bits. Essa coincidência é o falso positivo, e a taxa é configurável. O InfiniFilter é um quotient filter que cresce. Em vez de ser recriado do zero quando enche, aumenta a estrutura e alonga a impressão digital (fingerprint) de cada chave. Os dois podem dizer que um CID talvez esteja presente. Só o fundo confirma. Um segundo filtro pequeno guarda ausências recentes, para não repetir a ida ao disco logo depois de um has negativo.
Os metadados ficam à parte, numa tabela H2 aberta por JDBC (Java Database Connectivity, a API Java de bancos relacionais): tamanho, links, o que block/stat precisa. O bloco em si continua no fundo.
No Nabu
EmbeddedIpfs.buildBlockStore escolhe o fundo pelo prefixo do config, em src/main/java/org/peergos/EmbeddedIpfs.java. flatfs.datastore monta um FileBlockstore no diretório IPFS e o embrulha num CachingBlockMetadataStore junto com o metadata JDBC. Se updateMetadb é verdadeiro, updateMetadataStoreIfEmpty percorre os blocos quando a tabela ainda está vazia. s3.datastore faz o análogo com S3Blockstore, lendo região, bucket, rootDirectory e regionEndpoint dos parâmetros do mount. Outro prefixo lança Unrecognized datastore prefix.
O caminho de arquivo está em FileBlockstore.getFilePath. hashToKey codifica o multihash em base32 e corta no primeiro =. A pasta são os dois caracteres imediatamente anteriores ao último. O arquivo é a chave mais .data, e a raiz é o diretório blocks/ criado no construtor se ainda não existe:
String key = hashToKey(cid);
String folder = key.substring(key.length() -3, key.length()-1);
String filename = key + BLOCK_FILE_SUFFIX;
put grava com TRUNCATE_EXISTING e CREATE, e devolve o CID versão 1 com SHA-256 (Secure Hash Algorithm de 256 bits), como no primeiro post. has é file.exists(). hasAny procura o mesmo multihash sob os codecs DagCbor, Raw e DagProtobuf, montando um CID versão 1 para cada um. get lê o arquivo inteiro para um byte[], ou devolve vazio se o arquivo não está.
filteredBlockStore embrulha o fundo se config.datastore.filter.type for BLOOM ou INFINI, com a taxa de falso positivo do config. NONE devolve o fundo direto. Outro tipo é erro. FilteredBlockstore.has só chama o fundo quando o filtro de presentes responde sim e o de ausências responde não. Se o fundo diz não, o CID entra no filtro de ausências. Esse filtro recomeça depois de 10.000 ausências, com um CidInfiniFilter de 1.000 entradas e taxa 0,001. get segue a mesma porta: fora do filtro de presentes, devolve vazio sem ler o fundo. put grava no fundo e só então chama present.add. rm apaga no fundo e não mexe no filtro de presentes. Um CID removido pode continuar "talvez presente" até o filtro ser refeito. O has seguinte vai ao fundo, recebe não, e registra a ausência.
bloomAdd insere o CID no filtro de presentes, sem gravar bytes. É o que a API bloom/add faz. Sem isso, um CID que já está no fundo e ficou de fora do filtro seria invisível: has e get nem consultariam o disco. Se o fundo não tem o bloco, has responde falso e o CID entra no filtro de ausências. O filtro sozinho não afirma a posse.
O metadata nasce em buildBlockMetadata. A URL JDBC é jdbc:h2: mais o caminho de nabu-blockmetadata.sql, com MODE=PostgreSQL. A conexão fica embrulhada em UncloseableConnection para um close acidental não derrubar o banco, e o auto-commit fica ligado. TypeLimitedBlockstore entra por último, quando allowedCodecs não está vazio. put e rm de um codec fora da lista lançam Unsupported codec. get de um codec fora da lista devolve vazio, sem chamar o fundo.
Os exemplos usam RamBlockstore: um mapa na memória do processo, sem arquivo, sem S3 e sem filtro. O processo Nabu usa arquivo ou S3.
O que fica de fora: não há Badger, LevelDB nem outro sharding além da pasta de dois caracteres. O filtro pode afirmar presença à toa, dentro da taxa configurada. A ausência confirmada pelo fundo é a resposta que vale. O InfiniFilter do código segue o artigo de Pagh, cresce por gerações de fingerprint, e a série não reproduz a matemática. O ponto prático é que has barato continua correto o bastante para não ir ao S3 em todo want.
Nesta série
O anterior é IPNS, um nome que muda. O próximo é A API HTTP e como rodar o Nabu.
Top comments (0)