A natureza abstrata da camada de consenso da Cardano

Gostaria de entender um pouco mais sobre o PoS do projeto Cardano? Trouxemos abaixo um artigo traduzido por Joselmo Cabral (Postado originalmente em 28/05/2020 por Edsko de Vries, traduzido por Joselmo Cabral).

Esta nova série de postagens técnicas dos engenheiros da IOHK considera as escolhas que estão sendo feitas

Enquanto nos dirigimos para Shelley na rede principal de Cardano, queremos mantê-lo atualizado com as últimas notícias. Também queremos destacar o incansável trabalho em andamento nos bastidores e a equipe de engenheiros responsáveis.

Nesta série de blogs técnicos ocasionais do Developer Deep Dive, abrimos o espaço para os engenheiros da IOHK. Nos próximos meses, nossos desenvolvedores da Haskell oferecerão uma visão franca dos principais elementos da plataforma e dos protocolos Cardano e compartilharão insights sobre as escolhas que foram feitas.

Aqui, no primeiro da série, consideramos o uso da abstração na camada de consenso.

Introdução

camada de consenso de Cardano tem duas responsabilidades importantes:

  • Ele executa o protocolo de consenso blockchain. No contexto de um blockchain, consenso – ou seja, ‘maioria da opinião’ – significa que todos os envolvidos na execução do blockchain concordam com o que é a única cadeia verdadeira. Isso significa que a camada de consenso é responsável por adotar blocos, escolher entre cadeias concorrentes, se houver, e decidir quando produzir blocos próprios.
  • É responsável por manter todo o estado necessário para tomar essas decisões. Para decidir se deve ou não adotar um bloco, o protocolo precisa validá-lo com relação ao estado do ledger . Se decidir mudar para uma corrente diferente (um pedaço diferente de um fork na corrente), ele deverá manter histórico suficiente para poder reconstruir o estado do ledger nessa corrente. Para poder produzir blocos, ele deve manter um conjunto de memórias de transações a serem inseridas nesses blocos.

A camada de consenso medeia entre a camada de rede abaixo dela, que lida com preocupações como protocolos de comunicação e seleção de pares e a camada de ledger acima dela, que especifica como é o estado do ledger e como deve ser atualizado a cada novo bloco. A camada do ledger é totalmente sem estado e consiste exclusivamente em funções puras. Por sua vez, a camada de consenso não precisa saber a natureza exata do estado do ledger, ou mesmo o conteúdo dos blocos (além de alguns campos de cabeçalho necessários para executar o protocolo de consenso).

Detalhes importantes da camada de consenso da Cardano

O uso extensivo é feito de abstração na camada de consenso. Isso é importante por vários motivos:

  • Ele permite que os programadores injetem falhas ao executar testes. Por exemplo, abstraímos o sistema de arquivos subjacente e usamos isso para testar a camada de armazenamento e simular todo tipo de falhas no disco. Da mesma forma, abstraímos com o tempo e usamos isso para verificar o que acontece com um nó quando o relógio do sistema de um usuário salta para frente ou para trás.
  • Isso nos permite instanciar a camada de consenso com muitos tipos diferentes de ledgers. Atualmente, usamos isso para executar a camada de consenso com o ledger Byron (o ledger que está atualmente no blockchain Cardano), bem como o ledger Shelley para a próxima rede de testes Shelley Haskell. Além disso, usamos-o para executar a camada de consenso com vários tipos de ledgers especificamente projetados para teste, geralmente muito mais simples que os ledgers ‘reais’, para que possamos focar nossos testes na própria camada de consenso.
  • Ele melhora a composição , permitindo construir componentes maiores a partir de componentes menores. Por exemplo, o ledger de teste da Shelley contém apenas o ledger de Shelley, mas quando o Shelley for liberado, a cadeia real conterá o livro de Byron até o ponto do hard fork e o livro de Shelley a partir de então. Isso significa que precisamos de uma camada de ledger que alterne entre dois ledgers em um ponto específico. Em vez de definir um novo ledger, podemos definir um combinador de hard fork que implementa precisamente essa e somente essa funcionalidade. Isso melhora a reutilização do código (não precisaremos reimplementar a funcionalidade do hard fork quando o próximo hard fork aparecer), bem como a separação de preocupações: o desenvolvimento e o teste do combinador de hard fork não dependem das especificidades dos ledgers entre eles.
  • Intimamente relacionado ao último ponto, o uso da abstração melhora a testabilidade . Podemos definir combinadores que definem pequenas variações de protocolos de consenso que nos permitem focar em aspectos específicos. Por exemplo, temos um combinador que aceita um protocolo de consenso existente e substitui apenas a verificação se devemos produzir um bloco. Em seguida, podemos usar isso para gerar cenários de teste nos quais muitos nós produzem um bloco em um determinado slot ou, inversamente, nenhum nó, e verificar se a camada de consenso faz a coisa certa nessas circunstâncias. Sem um combinador para substituir esse aspecto da camada de consenso, seria deixado ao acaso se tais cenários surgissem. Embora possamos controlar o ‘acaso’ no teste (escolhendo uma semente inicial específica para o gerador de números aleatórios pseudo-aleatórios), seria impossível configurar cenários específicos, ou mesmo reduzir automaticamente os casos de teste com falha para um caso de teste mínimo. Com esses combinadores de consenso de teste, o teste pode literalmente usar apenas uma programação especificando quais nós devem produzir blocos em quais slots. Podemos então gerar esses agendamentos aleatoriamente, executá-los e, se houver um erro, reduzi-los a um caso de teste mínimo que ainda aciona o bug. Um cronograma de falha tão mínimo é muito mais fácil de depurar, entender e trabalhar do que apenas uma escolha de semente aleatória inicial que faz com que … algo … aconteça … em algum momento .

Para conseguir tudo isso, no entanto, a camada de consenso precisa fazer uso de alguns tipos sofisticados de Haskell. Nesta postagem do blog, desenvolveremos um ‘mini protocolo de consenso’, explicaremos exatamente como abstraímos vários aspectos e como podemos fazer uso dessas abstrações para definir vários combinadores. O restante deste post do blog pressupõe que o leitor esteja familiarizado com Haskell.

Preliminares

A camada de consenso de Cardano foi projetada para suportar a família de protocolos de consenso Ouroboros, todos assumindo fundamentalmente que o tempo é dividido em slots , onde o blockchain pode ter no máximo um bloco por slot. Neste artigo, definimos um número de slot para ser apenas um Int:

Também precisaremos de um modelo mínimo de criptografia de chave pública, especificamente

A única extensão de idioma que precisaremos nesta postagem de blog é TypeFamilies, embora a implementação real use muito mais. Se você quiser acompanhar, faça o download do código fonte completo.

Protocolo de consenso

Conforme mencionado no início deste post, o protocolo de consenso tem três responsabilidades principais.

  • Verificação do líder (devemos produzir um bloco?)
  • Seleção da cadeia
  • Bloquear verificação

O protocolo pretende ser independente de uma escolha concreta de bloco, bem como de uma escolha concreta do ledger, de modo que um único protocolo possa ser executado com diferentes tipos de blocos e / ou registros. Portanto, cada uma dessas três responsabilidades define sua própria ‘visão’ sobre os dados necessários.

Verificação do líder

A verificação do líder é executada em todos os slots e deve determinar se o nó deve produzir um bloco. Em geral, a verificação do líder pode exigir algumas informações extraídas do estado do ledger:

Por exemplo, no protocolo de consenso de Ouroboros Praos que inicialmente fornecerá a Shelley, a probabilidade de um nó ser eleito líder (ou seja, permitir que produza um bloco) depende de sua participação; o stake do nó, é claro, vem do estado do ledger.

Seleção da cadeia

‘Seleção de cadeias’ refere-se ao processo de escolha entre duas cadeias concorrentes. O principal critério aqui é o comprimento da cadeia, mas alguns protocolos podem ter requisitos adicionais. Por exemplo, os blocos são normalmente assinados por uma chave ‘quente’ existente no servidor, que por sua vez é gerada por uma chave ‘fria’ que nunca existe em nenhum dispositivo em rede. Quando a tecla de atalho é comprometida, o operador do nó pode gerar um novo a partir da tecla de atalho e ‘delegar’ para essa nova chave. Portanto, em uma escolha entre duas cadeias de comprimento igual, ambas com uma ponta assinada pela mesma tecla de atalho, mas com uma tecla de atalho diferente, um protocolo de consenso preferirá a tecla de atalho mais recente. Para permitir que protocolos de consenso específicos indiquem requisitos como esse, introduzimos um SelectView, especificando quais informações o protocolo espera que estejam presentes no bloco:

Validação de cabeçalho

A validação de bloco é principalmente uma preocupação do ledger; verificações como a verificação de que todas as entradas de uma transação estão disponíveis para evitar gastos duplos são definidas na camada do ledger. A camada de consenso não tem conhecimento do que está dentro dos blocos; na verdade, pode até não ser uma criptomoeda, mas uma aplicação diferente da tecnologia blockchain.

Os blocos (mais precisamente, os cabeçalhos dos blocos) também contêm algumas coisas especificamente para apoiar a camada de consenso. Para ficar com o exemplo do Praos, o Praos espera que várias provas criptográficas estejam presentes relacionadas à derivação da entropia da blockchain. A verificação destas é de responsabilidade do consenso e, portanto, definimos uma terceira e última visão dos campos que o consenso deve validar:

Demonstração da camada de consenso de Cardano 5
Demonstração da camada de consenso de Cardano 5

Definição de protocolo

Precisamos de mais uma família de tipos *: cada protocolo pode exigir informações estáticas para ser executado; chaves para assinar blocos, sua própria identidade para a verificação de líder, etc. Chamamos isso de ‘configuração do nó’ e a definimos como

Agora, podemos unir tudo na definição abstrata de um protocolo de consenso. Os três métodos da classe correspondem às três responsabilidades que mencionamos; cada método recebe a configuração do nó estático, bem como a visualização específica necessária para esse método. Observe que p aqui é simplesmente uma tag de nível de tipo que identifica um protocolo específico; nunca existe no nível de valor.

Exemplo: BFT permissivo (PBFT)

O BFT permissivo é um protocolo de consenso simples, no qual os nós produzem blocos de acordo com uma programação de rodízio: primeiro nó A, depois B, depois C e depois volta para A. Chamamos o índice de um nó nessa programação como ‘NodeId’. Este é um conceito apenas para PBFT, não algo que o restante da camada de consenso precisa estar ciente:

A configuração do nó exigida pelo PBFT é a própria identidade do nó, bem como as chaves públicas de todos os nós que podem aparecer no planejamento:

Como o PBFT é uma programação simples de rodízio, ele não precisa de nenhuma informação do ledger:

A seleção da corrente apenas analisa o comprimento da corrente; por uma questão de simplicidade, assumiremos aqui que cadeias mais longas terminam em um número maior de slots ** e, portanto, a seleção de cadeias precisa apenas saber o número de slot da ponta:

Por fim, ao validar blocos, devemos verificar se eles são assinados por qualquer um dos nós com permissão para produzir blocos (que podem aparecer no cronograma):

Definir o protocolo agora é simples:

A seleção de cadeias apenas compara os números dos slots; a validação verifica se a chave é um dos líderes de slot permitidos e a seleção de líderes faz alguma aritmética modular para determinar se é a vez desse nó.

Observe que o cronograma round-robin é usado apenas para verificar se somos ou não um líder; para blocos históricos (ou seja, ao fazer a validação), apenas verificamos se o bloco foi assinado por qualquer um dos líderes permitidos, e não em qual ordem. Esta é a parte ‘permissiva’ da PBFT; a implementação real faz uma verificação adicional (de que não há um único líder assinando mais do que seu compartilhamento), que omitimos nesta postagem do blog.

Exemplo de combinador de protocolo: planejamento explícito do líder

Conforme mencionado na introdução, para testar, é útil poder decidir explicitamente a ordem na qual os nós assinam blocos. Para fazer isso, podemos definir um combinador de protocolo que aceita um protocolo e simplesmente substitui a verificação is-leader para verificar uma programação fixa:

A configuração necessária para executar esse protocolo é o agendamento e o ID do nó nesse agendamento, além de qualquer configuração que o protocolo subjacente p exija:

A seleção de cadeias permanece inalterada e, portanto, também é a visão sobre os blocos necessários para a seleção de cadeias:

No entanto, precisamos substituir a validação do cabeçalho – na verdade, desabilitá-la completamente – porque se o protocolo subjacente verificar que os blocos estão assinados pelo nó correto, obviamente essa verificação seria incompatível com essa substituição. Portanto, não precisamos de nada do bloco para validá-lo (já que não fazemos nenhuma validação):

Definir o protocolo agora é trivial:

A seleção em cadeia usa apenas a seleção em cadeia do protocolo subjacente p, a validação de bloco não faz nada e a verificação is-leader refere-se à programação estática.

De blocos a protocolos

Assim como o protocolo de consenso, uma escolha específica de bloco também pode vir com seus próprios dados de configuração necessários:

Muitos blocos podem usar o mesmo protocolo, mas um tipo específico de bloco é projetado para um tipo específico de protocolo. Portanto, apresentamos uma família de tipos mapeando um tipo de bloco para o protocolo associado:

Dizemos então que um bloco suporta seu protocolo se pudermos construir as visualizações nesse bloco exigidas por seu protocolo específico:

Exemplo: Blocos Byron (simplificados)

De forma bruta, os blocos da blockchain Byron parecem algo como

Mencionamos acima que a família de protocolos de consenso de Ouroboros assume que o tempo é dividido em slots . Além disso, eles também assumem que esses slots são agrupados em épocas . Os blocos de Byron não contêm um número absoluto, mas um número de época e um número de slot relativo nessa época. O número de slots por época na cadeia Byron é fixado em um pouco acima de 10 k , mas para testar é útil poder variar k , o parâmetro de segurança e, portanto, é configurável; esta é (parte da) configuração necessária para trabalhar com os blocos Byron:

Dada a configuração de Byron, é simples converter o par de um número de época e um slot relativo em um número absoluto de slot:

E o protocolo? Bem, a cadeia Byron executa PBFT, e assim podemos definir

É fácil provar que o bloco Byron suporta este protocolo:

O estado do ledger

A camada de consenso não apenas executa o protocolo de consenso, mas também gerencia o estado do ledger. No entanto, não se importa muito com a aparência específica do estado do ledger; em vez disso, assume apenas que algum tipo de estado do ledger está associado a um tipo de bloco:

Vamos precisar de uma família de tipos adicionais. Quando aplicamos um bloco ao estado do ledger, podemos obter um erro se o bloco for inválido. O tipo específico de erros de ledger é definido na camada do ledger e, é claro, é altamente específico. Por exemplo, o ledger de Shelley terá erros relacionados à aplicação de stakes, enquanto o ledger de Byron não, porque não suporta a aplicação de stakes; e ledgers que não são criptomoedas terão tipos muito diferentes de erros.

Agora, definimos duas classes de tipos. O primeiro apenas descreve a interface para a camada do ledger, dizendo que devemos poder aplicar um bloco ao estado do ledger e obter um erro (se o bloco for inválido) ou o estado do ledger atualizado:

Segundo, definiremos uma classe de tipo que conecta o ledger associado a um bloco ao protocolo de consenso associado a esse bloco. Assim como o BlockSupportsProtocol fornece funcionalidade para derivar visualizações do bloco exigido pelo protocolo de consenso, o LedgerSupportsProtocol da mesma forma fornece funcionalidade para derivar visualizações do estado do ledger exigido pelo protocolo de consenso:

Veremos na próxima seção por que é útil separar a integração com o ledger (UpdateLedger) de sua conexão com o protocolo de consenso (LedgerSupportsProtocol).

Combinadores de bloco

Como exemplo final do poder dos combinadores, mostraremos que podemos definir um combinador em blocos e seus ledgers associados. Como um exemplo de onde isso é útil, o blockchain Byron vem com uma implementação e uma especificação executável. É útil instanciar a camada de consenso com esses dois livros, para que possamos verificar se a implementação concorda com a especificação em todos os pontos ao longo do caminho. Isso significa que os blocos nessa configuração de ‘ledger duplo’ são, de fato, um par de blocos ***.

A definição do estado do ledger duplo e do erro do ledger duplo é semelhante:

Para aplicar um bloco duplo ao estado do ledger duplo, simplesmente aplicamos cada bloco ao seu estado associado. Esse combinador em particular assume que os dois ledgers devem sempre concordar se um bloco específico é ou não válido, o que é adequado para comparar uma implementação e uma especificação; outras opções (outros combinadores) também são possíveis:

Como a intenção do ledger duplo é comparar duas implementações do ledger , basta que todas as preocupações de consenso sejam direcionadas pelo primeiro bloco (‘principal’); não precisamos de uma instância para ProtocolLedgerView para o bloco auxiliar e, de fato, em geral, talvez não seja possível fornecer uma. Isso significa que o protocolo de bloco do bloco duplo é o protocolo de bloco do bloco principal:

A configuração de bloco que precisamos é a configuração de bloco de ambos os blocos:

Agora podemos mostrar facilmente que o bloco duplo também suporta seu protocolo:

Conclusões sobre Consenso da Cardano

A camada de consenso de Cardano foi projetada inicialmente para o blockchain Cardano, atualmente executando Byron e logo executará Shelley. Você pode argumentar que isso significa que os engenheiros da IOHK devem projetar inicialmente para essa blockchain específica e generalizar apenas mais tarde ao usar a camada de consenso para outras blockchains. No entanto, fazer isso teria desvantagens importantes:

  • Isso prejudicaria nossa capacidade de realizar testes. Não poderíamos substituir a programação de quais nós produzem blocos quando, não poderíamos executar com um ledger duplo, etc.
  • Nós envolveríamos coisas que são logicamente independentes. Com a abordagem abstrata, o ledger Shelley consiste em três partes: uma cadeia de Byron, uma cadeia de Shelley e um combinador de fork que medeia entre esses dois. Sem abstrações, essa separação de preocupações seria mais difícil de alcançar, levando a mais difícil compreensão e manutenção do código.
  • O código abstrato é menos propenso a erros. Como um exemplo simples, como o combinador de ledger duplo é polimórfico nos dois ledgers combinados e eles têm tipos diferentes , não conseguimos escrever o código correto do tipo que, no entanto, tenta aplicar, por exemplo, o bloco principal ao ledger auxiliar.
  • Quando chegar a hora e queremos instanciar a camada de consenso para um novo blockchain (como inevitavelmente será), escrevê-la de uma maneira abstrata desde o início nos obriga a pensar cuidadosamente sobre o design e evitar o acoplamento de coisas que não devem ser acopladas , ou fazer suposições que sejam justificadas para o blockchain real em consideração, mas podem não ser verdadeiras em geral. Corrigir esses problemas após a implantação do design pode ser difícil.

É necessário uma linguagem de programação com boas habilidades

Obviamente, tudo isso requer, de fato, uma linguagem de programação com excelentes habilidades de abstração; felizmente, Haskell se encaixa perfeitamente nessa conta.

Este é o segundo de uma série de posts do Deep Dive voltados para desenvolvedores

  • Diferente das várias visualizações, o NodeConfig é uma família de dados; como a configuração do nó é passada para todas as funções, ter o NodeConfig como uma família de dados ajuda a digitar inferência, pois determina p. Deixar o resto como famílias de tipos pode ser conveniente e evitar a embalagem e a embalagem desnecessárias.

** Os números dos slots são um proxy para o comprimento da cadeia somente se todos os slots contiverem um bloco. Isso é verdade para PBFT apenas na ausência de partições de rede temporárias e não é verdade para algoritmos probabilísticos de consenso, como o Praos. Dito de outra forma, uma cadeia de densidade mais baixa pode muito bem ter um número de slot mais alto na ponta do que outra cadeia de densidade mais curta mas mais alta.

*** O verdadeiro combinador DualBlock mantém uma terceira informação que relaciona os dois blocos (como identificações de transação). Nós o omitimos neste post por questão de simplicidade.

Comunidade ADA Brasil

A comunidade ADA Brasil está presente no Telegram e, além disso, temos um blog com vários materiais disponíveis para leitura. O tradutor deste texto é Joselmo Cabral.

Por fim, este texto foi promovido por colaboradores da Comunidade ADA Cardano – Brasil, única endossada pela equipe oficial no país. Confira também nosso canal no YouTube:

Saiba mais em A natureza abstrata da camada de consenso da Cardano

Você pode gostar...