ZYCORD docs
Português
ZycordDocumentaçãoProtocolo de transporte

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.

Esta página é um guia, não a especificação

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
  • length tem de ser conferido contra MaxMessageBytes = 8 MiB 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.
  • kind tem de estar em [1, 9]. Zero e qualquer coisa acima do maior tipo conhecido são uma violação de protocolo, não uma extensão desconhecida: não há válvula de escape para compatibilidade futura no protocolo 1, porque um campo de versão conferido no handshake torna uma desnecessária.

Tipos de mensagem#

ValorNomeDireçãoCarga útil
1helloambos, primeiroHandshake
2certificategossipssz(Certificate)
3block-announcegossipCabeçalho mais ids de certificado
4get-blockrequisiçãoid de bloco de 32 bytes ‖ índice de chunk u32
5blockrespostaUm chunk de ssz(Block)
6get-headersrequisiçãoLocalizador
7headersrespostaSequência de cabeçalhos
8get-peersrequisiçãovazia
9peersrespostaLista de endereços

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.

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.

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#

AusentePor quê
Travessia de NATO 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ãoUm 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 mensagemO 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çãoRequisiçã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.
Um mecanismo de extensão com compatibilidade futuraO protocolo 1 confere sua versão no handshake e desconecta em caso de divergência. Extensões chegam como protocolo 2.

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.