Protocolo de transporte
O que os nós do Zycord dizem uns aos outros, e as regras que impedem um protocolo de pares de virar um amplificador. Um guia para a especificação normativa, não um substituto dela.
A especificação é spec/: os arquivos de parâmetros e o corpus de vetores. Onde esta página e a árvore divergirem, a árvore é que está certa — uma implementação é conforme quando reproduz o id de gênese e passa pelos vetores, e nada aqui muda isso. O que segue é um mapa de onde estão os requisitos.
Transporte#
TCP, depois TLS 1.3. A identidade de par é uma chave Ed25519 carregada em um certificado X.509 autoassinado, apresentado por ambos os lados. Cadeias de certificados não são validadas — não há autoridade certificadora e nada para ela atestar — então a verificação exige exatamente um certificado, exige que sua chave pública seja Ed25519, e extrai essa chave como a identidade do par.
A propriedade obtida é vinculação do canal a uma chave, não autorização: o TLS garante que quem completou o handshake detém a chave privada da identidade apresentada, e que o fluxo não pode ser lido nem modificado em trânsito. Não diz nada sobre se esse par é honesto. Nada no protocolo concede autoridade com base em identidade.
As datas de validade do certificado são constantes fixas em vez de uma janela em torno do horário atual, de propósito: uma janela relativa faz a aceitação do certificado depender de concordância de relógio, e uma rede que particiona ao longo do desvio de relógio é uma rede que particiona por um motivo alheio ao consenso.
As implementações não podem reutilizar uma chave de qualquer outra camada como identidade de par. O nó gera uma nova a cada início e nunca a escreve em disco.
Enquadramento#
offset size field
0 4 length uint32, little-endian - payload length, excluding this header
4 1 kind uint8 - see below
5 n payload
lengthtem de ser conferido contraMaxMessageBytes= 8 MiB, e contra o teto do seu próprio tipo, antes de qualquer alocação. Um receptor que aloca com base num comprimento alegado tem um bug remoto de exaustão de memória, independentemente do que venha depois.- O
kind0 é recusado. Um tipo acima do maior que uma build conhece é um que uma build mais nova acrescentou: seu payload é lido e descartado sobMaxUnknownPayloadBytes= 64 KiB, a uma taxa limitada por par, em vez de a conexão ser fechada. Acima dessa taxa, ele é pontuado como uma requisição excessiva.
Tipos de mensagem#
| Valor | Nome | Direção | Carga útil |
|---|---|---|---|
| 1 | hello | ambos, primeiro | Handshake |
| 2 | certificate | gossip | ssz(Certificate) |
| 3 | block-announce | gossip | Cabeçalho mais os hashes ordenados dos exemplares de certificado |
| 4 | get-block | requisição | id de bloco de 32 bytes ‖ índice de chunk u32 |
| 5 | block | resposta | Um chunk de ssz(Block) |
| 6 | get-headers | requisição | Localizador |
| 7 | headers | resposta | Sequência de cabeçalhos |
| 8 | get-peers | requisição | vazia |
| 9 | peers | resposta | Lista de endereços |
| 10 | service-card | gossip, por capacidade | Um anúncio de serviço assinado |
| 11 | block-unavailable | resposta | Id do bloco ‖ motivo: abaixo do piso de corpos do remetente, ou nunca mantido |
| 12 | open-intent | gossip, por capacidade | Um intent aberto para o caminho forçado |
Não existe codificação específica de rede para um objeto de consenso, e é isso que torna "o id do que recebi" uma afirmação verificável.
A versão do protocolo é 3, e o handshake exige correspondência exata. O hello a carrega
junto com o id da rede — o id de gênese, que se compromete com todos os parâmetros de consenso —
a altura e o trabalho do remetente, seu endereço de escuta, uma palavra de capacidades e seu piso
de corpos, a menor altura cujo corpo de bloco ele serve. Um tipo condicionado a uma capacidade nunca é
enviado a um par que não a anunciou, e um bit de capacidade que uma build não define é ignorado,
nunca recusado. Um get-block que o receptor não vai atender é respondido com
block-unavailable em vez de ser deixado para o prazo do requisitante.
Um open-intent é o único tráfego do caminho forçado: um intent de usuário sem sequenciador
nomeado, que qualquer produtor pode executar no fim do seu bloco. O certificado forçado resultante
nunca é propagado por gossip como certificado; ele nasce no bloco, e todo nó o verifica ali.
Toda mensagem recebida custa ao seu remetente#
Esta é a parte da especificação que mais vale a leitura completa, porque é onde um protocolo de pares costuma vazar. Duas regras a governam.
A regra de ordenação por custo#
O trabalho é feito em ordem crescente de custo, e uma mensagem é cobrada antes da etapa cara que a segue. Um receptor que verifica primeiro e pontua depois construiu um amplificador: o barato de enviar é o caro de checar.
Todo resultado tem preço#
Não existe um Free sem nome. Cada tipo de mensagem, cruzado com cada
resultado que ele pode produzir, aparece em uma tabela com uma pontuação associada. Um resultado que atravessa a
tabela sem ser cobrado é o defeito que esta regra existe para evitar — e ele já foi
alcançado na prática por caminhos mais estreitos que uma linha sem preço: uma implementação que desviou um
quadro malformado para longe de seu único pontuador tornou aquele quadro gratuito enquanto houvesse uma requisição
pendente, sem acrescentar uma linha a tabela alguma.
O atendimento também é medido. Os bytes de bloco são a única resposta ordens de magnitude maior que a requisição que a pede, então eles carregam seu próprio orçamento de bytes além da contagem de requisições.
As pontuações se ligam à identidade de um par: uma mensagem inválida custa 20, uma violação de protocolo 50, um corpo não servido 10 e uma requisição excessiva 5; uma mensagem útil ganha 1, e um par é banido em −100. Verificações caras — épocas de chave e avaliações de proof of work — têm orçamento por par e são reembolsadas quando o bloco se mostra útil, de modo que um anúncio que não leva a lugar nenhum não pode fazer um nó moer trabalho.
Retransmissão e sincronização#
A retransmissão é por hash primeiro. Os certificados circulam por gossip por conta própria e têm sua validade verificada, sem estado e em paralelo, na chegada; um retransmissor não precisa de estado para filtrar spam. Os blocos viajam como um cabeçalho mais os hashes ordenados dos exemplares, contra o mempool do receptor, e o receptor verifica o trabalho do cabeçalho antes de buscar qualquer corpo. Um corpo viaja em pedaços de 4 MiB, cada um com sua própria requisição e resposta, de modo que o limite de quadro limita uma mensagem e nunca um bloco. Os corpos são dados da cadeia: um bloco só é válido se seus corpos puderem ser obtidos, e um nó que faz poda informa onde está seu piso no handshake.
A sincronização escolhe pelo trabalho verificado. Os cabeçalhos são validados como uma cadeia antes de os corpos serem buscados, e um nó prefere o par cujos cabeçalhos provaram o maior trabalho àquele que alega a maior altura. Um ramo concorrente só é adotado quando se conecta dentro do horizonte de desfazer de 1.024 blocos e carrega mais trabalho. Checkpoints fixados na release, aceitos apenas para blocos com pelo menos uma época de idade, recusam uma cadeia que os contradiga, por mais pesada que seja; um operador pode acrescentar um checkpoint por flag. A partir da transição para proof of stake na era 2, a escolha passa a ser por comprimento: vence a cadeia que ocupa mais slots (§11 do whitepaper), e o horizonte de desfazer e os checkpoints fixados permanecem, como a defesa contra a reescrita com chaves cuja caução já foi retirada.
Gestão de conexões, e a defesa contra eclipse#
Esta seção carrega mais falhas medidas que qualquer outra, e cada requisito abaixo existe porque uma implementação sem ele foi eclipsada em uma medição.
- Os alvos de saída precisam ser selecionados com diversidade de endereços, para que uma única faixa de hospedagem não possa preencher os slots de saída de um nó.
- Os alvos de saída também precisam ser limitados por fonte de gossip. Diversidade de endereços por si só não basta, por um motivo que é aritmético e não de julgamento: um grupo de endereços é uma propriedade de um endereço que um atacante possui, mas um endereço que um par alega são bytes que ele inventou, e quaisquer quatro bytes são um host IPv4 válido. Um atacante sem endereço algum cunha um novo grupo de diversidade por string ao preço de um quadro. O que ele não pode inventar é a conexão pela qual a alegação chegou.
- O socket pelo qual uma conexão de entrada chegou não pode virar um alvo de saída. Ele é o endereço de origem do par — uma porta efêmera que o SO dele escolheu, não algo em que ele escute — então um slot gasto discando para ele é um slot gasto num endereço que não pode responder.
- Os dois limites precisam ser contados contra as conexões que um nó mantém, não contra uma única chamada de seleção. Caso contrário, um laço de discagem que exclui pares já conectados reconstrói os dois orçamentos cheios a cada rodada, e o limite atrasa um atacante em uma rodada por cota em vez de limitá-lo. Medido: um informante tomou 2, depois 4, depois 6, depois 8 de 8 slots de saída ao longo de quatro rodadas.
- O armazenamento de pares precisa ser persistido, e precisa ser limitado. Um nó que começa em branco depois de cada reinício entrega a um atacante uma oportunidade nova a cada reinício; um que começa com a lista do atacante entrega a ele a mesma oportunidade permanentemente.
- Um armazenamento limitado não pode recusar um endereço bem formado por estar cheio; ele remove alguém para abrir espaço. Um endereço honesto nunca contatado nunca é melhor que um inventado nunca contatado, então um atacante que encher o armazenamento primeiro o tranca contra tudo o que for oferecido depois — inclusive a própria lista de bootstrap do operador.
- O que a remoção escolhe importa mais do que o fato de ela acontecer. "Tire a vítima da maior população" se lê como uma inundação desloca a si mesma, e só é isso enquanto a inundação for a maior coisa no armazenamento. Num nó que fez bootstrap a partir de um par prestativo, a maior população é a agenda de endereços desse par. Medido numa implementação ordenada dessa forma: 200 endereços inventados de uma única fonte removeram 200 honestos sem custo algum para o atacante.
- Entre entradas indistinguíveis, o critério final de desempate não pode ser nada que o par que faz gossip escolha — o endereço incluído — e isso vale para a seleção exatamente como vale para a remoção. Medido numa implementação cujo seletor caía para a string do endereço: 8 endereços honestos contra 8 inventados devolveram 8 de 8 inventados, e zero conexões de saída honestas ao longo de dez rodadas.
O que está deliberadamente ausente#
| Ausente | Por quê |
|---|---|
| Travessia de NAT | O custo é declarado e a condição de reabertura é medida em vez de presumida: a proporção de nós acessíveis na testnet pública é a primeira condição para reabrir a decisão. |
| Compressão | Um compressor num fluxo controlado pelo atacante é uma superfície de ataque, por uma economia de banda cuja necessidade ninguém mediu. |
| Assinaturas em nível de mensagem | O transporte autentica o canal; os objetos de consenso carregam as próprias assinaturas. Uma terceira camada autenticaria o retransmissor, o que não é algo de que qualquer decisão dependa. |
| Ids de requisição | Requisição e resposta são casadas por conexão e ordem, então a sincronização roda na própria conexão — o que elimina uma máquina de estados. Onde um par não for acessível, a sincronização pode rodar sobre uma conexão de gossip existente e precisa então casar respostas por conteúdo. |
| Negociação de versão | O handshake exige a versão exata do protocolo e desconecta em caso de divergência. O que evolui sem aumento de versão é estreito e limitado: um novo tipo de mensagem atrás de um bit de capacidade, que uma build mais antiga lê e descarta sob 64 KiB a uma taxa limitada. |
O que o id de um certificado não cobre#
Vale repetir aqui porque isso decide uma regra de retransmissão. O id de um certificado se compromete com o que ele autoriza e nunca com o que ele apenas demonstra, então as assinaturas ficam fora da preimagem do id. Isso torna a evidência algo que qualquer um em trânsito pode substituir: pegue um certificado, troque sua assinatura por lixo, e propague — mesmo id, exemplar agora inválido.
Duas regras fecham isso. Um bloco se compromete com a evidência que carrega, através de uma raiz sobre hashes de exemplares em vez de sobre ids. E a retransmissão trata exemplares, não ids: um nó que recebe um exemplar cuja evidência falha na verificação descarta aquele exemplar sem prejuízo ao id — o id não é marcado, não é armazenado como inválido, e um exemplar posterior que verifique é retransmitido normalmente. Uma cópia mutilada custa ao seu mutilador a banda de enviá-la e não custa nada ao certificado.
Implementando isto do zero#
Uma implementação independente que passe nos
vetores de referência é um par, não um fork — esse
é todo o objetivo de especificar deste jeito. Comece pelo
spec/README.md para os objetos de consenso, e depois confira
seu trabalho com zcd vectors.