Resiliência e Auto-recuperação

Uma rede descentralizada é tão durável quanto a sua capacidade de recuperar de falhas parciais. Os servidores ficam offline. Novos servidores entram e precisam de fazer o bootstrap dos seus dados. As partições de rede causam divergência. O Hashiverse aborda isto através de protocolos de auto-recuperação que operam sem qualquer coordenador central — conduzidos por clientes e servidores que detetam inconsistência e a reparam.

Sharding e distribuição de carga

As redes sociais seguem leis de potência. Um pequeno número de utilizadores e hashtags atrai a vasta maioria dos seguidores e gera a maioria das publicações. Numa DHT ingénua, isto significaria que um pequeno número de nós suportaria a maioria da carga — tornando-se em gargalos de desempenho e em alvos de DDoS de alto valor em virtude do conteúdo que alojam. O Hashiverse aborda isto ao nível do modelo de dados, através de sharding baseado em tempo e frequência.

Chaves de balde baseadas em épocas

O conteúdo de um utilizador ou hashtag não é guardado sob uma única chave DHT fixa. A chave é uma função do ID público do utilizador (ou texto do hashtag) e de uma época temporal. Isto significa que a localização DHT das publicações de um utilizador desloca-se ao longo do tempo, espalhando a responsabilidade de alojar o seu conteúdo por nós diferentes em épocas diferentes.

Para utilizadores e hashtags de alto volume, o espaço de chaves é ainda mais subdividido: quanto mais ocupada uma identidade for em termos de volume de publicações, mais granular se torna a subdivisão dos baldes. O conteúdo de um publicador prolífico é dividido por mais baldes, mais pequenos — cada um alojado por uma região diferente do anel DHT. Isto está capturado no tipo BucketLocation, que codifica em conjunto a identidade, a época e a granularidade do balde.

O resultado é que nenhum nó se torna permanentemente responsável pelo tráfego de um utilizador-poder, e nenhum nó se torna num alvo estável para um ataque DDoS visando silenciar uma conta ou hashtag específico.

Origem: buckets.rsBucketLocation

Caching guiado pela procura

Para além da auto-recuperação (que repara dados em falta a posteriori), o Hashiverse implementa caching guiado pela procura para reduzir a carga sobre os servidores DHT mais próximos para baldes populares. Servidores intermediários colocam em cache bundles de publicações e bundles de feedback em nome dos nós mais próximos do conteúdo, servindo os clientes diretamente e terminando o passeio Kademlia mais cedo. A cache expande para fora sob procura sustentada e contrai automaticamente quando o interesse cai — totalmente autorregulada, sem coordenação necessária.

Mecanismo de token por limiar de hits

Cada servidor mantém uma cache de bundles e uma cache de feedback (uma cache ponderada Moka por tipo). Em cada pedido recebido de GetPostBundleV1 ou GetPostBundleFeedbackV1, o servidor incrementa um contador de hits para essa localização de balde. Assim que o contador atinge o limiar (atualmente 10 hits dentro da janela de inatividade), o servidor emite um CacheRequestTokenV1 — um token de curta duração assinado pelo servidor e com âmbito limitado a essa localização de balde.

O cliente recolhe quaisquer tokens emitidos durante o seu passeio Kademlia. Após obter e verificar com sucesso o bundle de um servidor responsável, carrega assincronamente o bundle para cada servidor que emitiu um token, via CachePostBundleV1 / CachePostBundleFeedbackV1. O servidor valida o token (assinatura, expiração, correspondência de localização), faz parsing do bundle e guarda-o na cache em memória. Os clientes seguintes que passem por esse servidor recebem o bundle em cache diretamente, sem continuarem o passeio para dentro.

Uma cache de desduplicação em voo (chaveada por location ID, com TTL correspondente ao tempo de vida do token) impede o servidor de emitir tokens duplicados a vários caminhantes concorrentes para o mesmo balde.

Origem: post_bundle_caching.rs, post_bundle_feedback_caching.rs

Baldes vivos vs. selados

A cache distingue entre dados vivos e selados:

Raio de descoberta do lado do cliente

Para tirar proveito da cache em expansão, cada cliente mantém por balde um raio de descoberta: a distância XOR ao servidor mais distante que devolveu com sucesso um bundle no passeio anterior. No passeio seguinte, o PeerIterator salta servidores mais próximos do que este raio e começa a consultar a partir da fronteira para fora — indo diretamente até onde a cache foi encontrada da última vez, contornando totalmente os servidores responsáveis (mais interiores).

O raio é atualizado após cada passeio para a distância XOR do respondente positivo mais distante. Se as camadas externas da cache tiverem entretanto expirado e nenhum servidor na fronteira responder, o passeio recorre ao próximo par mais próximo e o raio contrai naturalmente para refletir a profundidade atual da cache. O raio é mantido numa cache de time-to-idle, por isso baldes que não tenham sido visitados há algum tempo começam de novo a partir dos servidores responsáveis no acesso seguinte.

Eviction da cache e capacidade

Ambas as caches são ponderadas pelo tamanho real do bundle em bytes, suportadas pela política de admissão W-TinyLFU do Moka. Entradas placeholder (localizações que foram consultadas mas ainda não povoadas com um bundle) usam um peso fixo pequeno, para participarem no esboço de frequência sem consumirem capacidade real. Quando a marca de água alta de bytes é atingida, as localizações menos usadas em frequência são as primeiras a ser evicted. Cada localização tem ainda um time-to-idle de 60 segundos: uma localização que deixe de receber pedidos é evicted silenciosamente, repondo o seu contador de hits e contraindo o raio efetivo da cache.

A cache de bundles guarda até um número configurável de originadores por localização (atualmente 5). Se chegar um novo originador quando o limite por localização estiver cheio, o bundle a expirar mais cedo é substituído. Bundles selados (sem expiração) são tratados como sendo os últimos a expirar e só são deslocados por outros bundles selados.

Origem: post_bundle_caching_shared.rs, config.rsSERVER_POST_BUNDLE_CACHE_MAX_BYTES et al.

Auto-recuperação de bundles de publicações

Quando um cliente vai buscar publicações para um balde, consulta vários servidores e recolhe as suas respostas. Depois compara: que servidor tem publicações que outro servidor não tem? Para cada par dador-alvo em que o dador tem publicações que faltam ao alvo, o cliente organiza uma auto-recuperação em duas fases:

  1. Fase de claim: O cliente pergunta ao servidor-alvo de quais das publicações do dador é que precisa, usando um pedido HealPostBundleClaimV1. O servidor declara quais os IDs de publicações que lhe faltam.
  2. Fase de commit: O cliente (ou o dador, agindo como proxy) envia esses bytes específicos das publicações para o alvo via HealPostBundleCommitV1.

Isto corre em tarefas em segundo plano lançadas após a obtenção primária do cliente terminar. O utilizador vê o seu conteúdo sem esperar pela auto-recuperação; a rede repara-se em paralelo.

Origem: post_bundle_healing.rs, test_healing_post_bundles.rs

Auto-recuperação de feedback

Os sinais de feedback — gostos, denúncias, não-gostos — também divergem entre servidores à medida que se propagam pela rede. Uma denúncia pode chegar ao servidor A mas não ao servidor B. A auto-recuperação de feedback é tratada separadamente da auto-recuperação de publicações porque o feedback tem uma forma de dados diferente (o registo EncodedPostFeedbackV1 de 50 bytes) e uma regra de merge diferente.

A regra de merge para feedback é: para cada par (post_id, feedback_type), manter o sinal com a PoW mais alta entre todos os servidores. O cliente, depois de recolher feedback de vários servidores, calcula o máximo global e identifica os servidores que têm sinais mais fracos do que o máximo global. Em seguida, envia os sinais mais fortes para esses servidores via HealPostBundleFeedbackV1.

Esta é uma auto-recuperação numa única fase (sem passo de claim): o registo de feedback é suficientemente pequeno para que enviá-lo incondicionalmente seja mais barato do que a ida e volta de pedir primeiro.

Origem: post_bundle_feedback_healing.rs, test_healing_post_bundle_feedbacks.rs

Resistência a DDoS

O Hashiverse tem quatro camadas de proteção contra DDoS que intercetam ataques em pontos progressivamente mais cedo no ciclo de vida da ligação, desde a filtragem de pacotes ao nível do kernel até à limitação de taxa ao nível da aplicação.

Camada de kernel: blacklist com ipset

Para ataques sustentados ou graves, o servidor escala para descarte de pacotes ao nível do kernel via o ipset do Linux. IPs em blacklist são adicionados a um hash set com um timeout de cinco minutos; o iptables (ou nftables) descarta os pacotes que correspondem antes de chegarem à camada de aplicação, eliminando até o custo de aceitar e rejeitar a ligação TCP.

ipset create hashiverse_ddos_blacklist hash:ip timeout 300

O servidor em Rust adiciona IPs a este set usando chamadas não-bloqueantes Command::new("ipset") — a operação do kernel não bloqueia o tratamento de pedidos. Isto requer CAP_NET_ADMIN ou root em produção, gerido via Linux Ambient Capabilities.

Pré-TLS: limite de ligações e limite de slots por IP

Cada nova ligação TCP é verificada antes de o handshake TLS começar. Duas proteções correm neste ponto:

Ambas as verificações disparam antes de TlsAcceptor::accept ser chamado, por isso uma inundação de novas ligações de um único IP não consome CPU em negociação TLS. O servidor é deliberadamente apenas IPv4 por agora; o suporte para IPv6 requer rastreamento ao nível do prefixo (/64) para ser eficaz, o que ainda não está implementado.

Timeout do handshake TLS

Uma vez que uma ligação passa as proteções pré-TLS, o handshake TLS é envolvido num timeout duro. Um cliente que envie um ClientHello mas nunca complete o handshake — uma ligação lenta clássica ao nível TLS — é descartado após um número fixo de segundos, e a sua pontuação de pedidos defeituosos é incrementada, movendo-o em direção ao banimento ao nível da aplicação.

Defesa contra Slow Loris: timeouts de leitura de cabeçalhos e corpo

Após um handshake TLS bem-sucedido, dois timeouts adicionais protegem contra ataques de manutenção de ligações ao nível HTTP:

Camada de aplicação: cache de reputação de IPs

Os pedidos que passam por todos os pontos acima são rastreados por endereço IP usando uma cache em memória Moka. IPs que excedam o limiar de taxa de pedidos dentro de uma janela deslizante recebem uma resposta de erro RPC do Hashiverse e acumulam uma pontuação de pedidos defeituosos. Assim que a pontuação cruza um limiar, o IP é promovido para a blacklist do ipset, descendo para a camada de kernel para futuras ligações.

Origem: hashiverse-server-lib/src/network/transport/, config.rs

Abstração TimeProvider

A trait TimeProvider abstrai sobre o tempo real, permitindo o ScaledTimeProvider — um relógio de simulação acelerado no tempo, usado em testes. Os testes de integração que verificam comportamento de auto-recuperação ao longo de janelas de tempo que normalmente demorariam horas podem correr em segundos, escalando o tempo. É assim que a suite de testes de integração consegue verificar o ciclo de vida completo da auto-recuperação — incluindo a expiração de conteúdo antigo — sem atrasos de relógio de parede.

Origem: hashiverse-lib/src/tools/time_provider/